728x90
반응형
Rich Text Editor 설치
pip install django-ckeditor
pip install Pillow # 이미지 업로드를 위해 필요
settings.py
# settings.py
INSTALLED_APPS = [
...
'ckeditor',
'ckeditor_uploader',
'blog', # 우리가 만들 앱
]
# CKEditor 설정
CKEDITOR_UPLOAD_PATH = "uploads/"
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'full',
'height': 300,
'width': '100%',
},
}
# 미디어 파일 설정
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
models.py
# blog/models.py
from django.db import models
from ckeditor_uploader.fields import RichTextUploadingField
class Post(models.Model):
title = models.CharField(max_length=200)
content = RichTextUploadingField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
forms.py
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content']
views.py
# blog/views.py
from django.views.generic import ListView, CreateView, UpdateView, DetailView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
ordering = ['-created_at']
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('post_list')
class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('post_list')
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
urls.py
# project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('ckeditor/', include('ckeditor_uploader.urls')),
path('', include('blog.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# blog/urls.py
from django.urls import path
from .views import PostListView, PostCreateView, PostUpdateView, PostDetailView
urlpatterns = [
path('', PostListView.as_view(), name='post_list'),
path('post/new/', PostCreateView.as_view(), name='post_create'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
path('post/<int:pk>/edit/', PostUpdateView.as_view(), name='post_update'),
]
templates
<!-- templates/blog/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Blog{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
{{ form.media }}
</head>
<body>
<div class="container mt-4">
{% block content %}
{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<!-- templates/blog/post_list.html -->
{% extends 'blog/base.html' %}
{% block content %}
<div class="mb-4">
<h2>블로그 포스트</h2>
<a href="{% url 'post_create' %}" class="btn btn-primary">새 포스트 작성</a>
</div>
<div class="row">
{% for post in object_list %}
<div class="col-md-12 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ post.title }}</h5>
<p class="card-text text-muted">{{ post.created_at|date:"Y-m-d H:i" }}</p>
<a href="{% url 'post_detail' post.pk %}" class="btn btn-sm btn-primary">자세히 보기</a>
<a href="{% url 'post_update' post.pk %}" class="btn btn-sm btn-secondary">수정</a>
</div>
</div>
</div>
{% empty %}
<p>아직 작성된 포스트가 없습니다.</p>
{% endfor %}
</div>
{% endblock %}
<!-- templates/blog/post_form.html -->
{% extends 'blog/base.html' %}
{% block content %}
<h2>{% if form.instance.pk %}포스트 수정{% else %}새 포스트 작성{% endif %}</h2>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.title.id_for_label }}" class="form-label">제목</label>
{{ form.title }}
</div>
<div class="mb-3">
<label for="{{ form.content.id_for_label }}" class="form-label">내용</label>
{{ form.content }}
</div>
<button type="submit" class="btn btn-primary">저장</button>
<a href="{% url 'post_list' %}" class="btn btn-secondary">취소</a>
</form>
{% endblock %}
<!-- templates/blog/post_detail.html -->
{% extends 'blog/base.html' %}
{% block content %}
<div class="card">
<div class="card-body">
<h1 class="card-title">{{ object.title }}</h1>
<p class="text-muted">
작성일: {{ object.created_at|date:"Y-m-d H:i" }}
{% if object.updated_at != object.created_at %}
(수정됨: {{ object.updated_at|date:"Y-m-d H:i" }})
{% endif %}
</p>
<hr>
<div class="content">
{{ object.content|safe }}
</div>
</div>
</div>
<div class="mt-3">
<a href="{% url 'post_list' %}" class="btn btn-secondary">목록으로</a>
<a href="{% url 'post_update' object.pk %}" class="btn btn-primary">수정</a>
</div>
{% endblock %}
admin.py
# blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'created_at', 'updated_at']
search_fields = ['title', 'content']
이 샘플 프로그램의 특징:
- CKEditor를 사용한 리치 텍스트 편집 지원
- 이미지 업로드 기능 포함
- 게시물 작성, 수정, 조회 기능
- 반응형 디자인 (Bootstrap 사용)
- 관리자 페이지 통합
사용하기 전에:
- python manage.py makemigrations 실행
- python manage.py migrate 실행
- media 폴더가 프로젝트 루트에 생성되었는지 확인
- 필요한 static 파일들이 수집되었는지 확인

728x90
반응형
'코딩' 카테고리의 다른 글
| 영어는 발음일까, 소리일까? (0) | 2025.01.11 |
|---|---|
| 보이지 않는 것에 대한 두려움; 통합 모델링 언어 (2) | 2025.01.06 |
| 부자 문자 부리기 (0) | 2025.01.06 |
| django.views.generic 에 대하여 (1) | 2025.01.05 |
| 치매 예방: 화투 보다 코딩 (1) | 2025.01.05 |
