공지사항관리 기능 분리 및 개선
All checks were successful
Build And Test / build-and-push (push) Successful in 4m23s

This commit is contained in:
2025-01-25 18:02:49 +09:00
parent 06d1853fb0
commit af57b56e69
38 changed files with 710 additions and 188 deletions

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.14 on 2025-01-25 14:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_rename_content_post_contents'),
]
operations = [
migrations.AlterField(
model_name='post',
name='author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='blog_posts', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,5 +1,6 @@
from django.db import models
from django.conf import settings
from django.urls import reverse
from markdown_it import MarkdownIt
from taggit.managers import TaggableManager
@ -10,12 +11,20 @@ class Post(models.Model):
created_at = models.DateTimeField(auto_now_add=True) # 작성일
updated_at = models.DateTimeField(auto_now=True) # 수정일
tags = TaggableManager()
author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
on_delete=models.CASCADE,
related_name='blog_posts'
)
def render_markdown(self):
"""마크다운을 HTML로 변환"""
md = MarkdownIt()
return md.render(self.contents)
def get_absolute_url(self):
return reverse('blog:post_detail', args=[str(self.pk)])
def __str__(self):
return self.title

View File

@ -1,39 +0,0 @@
{% extends "components/base.html" %}
{% block title %}Create Post{% endblock %}
{% block main_area %}
<h1>Create New Post</h1>
<div class="row">
<!-- 왼쪽: 마크다운 에디터 -->
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary mt-3">Save</button>
</form>
</div>
<!-- 오른쪽: 미리보기 -->
<div class="col-md-6">
<h2>Preview</h2>
<div id="preview" class="border p-3" style="background: #f8f9fa; min-height: 300px;"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit();
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
});
</script>
{% endblock %}

View File

@ -22,7 +22,7 @@
</div>
<!-- 마크다운 에디터 -->
<h2>contents</h2>
<h2>Contents</h2>
<div class="col-md-12">
{{ form.contents }}
@ -41,9 +41,22 @@
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit();
const md = window.markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs
.highlight(str, {language: lang})
.value;
} catch (__) {}
}
return ''; // 기본 HTML 이스케이프 처리
}
});
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
@ -52,7 +65,16 @@
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조
document
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
{% endblock %}
</div>
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends "components/base.html" %}
{% block title %}Create Post{% endblock %}
{% block main_area %}
<h1 class="pt-3">Create New Post</h1>
<div class="container">
<div class="row">
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
</div>
<div class="mb-3">
{{ form.summary.label_tag }}
{{ form.summary }}
</div>
<div class="mb-3">
{{ form.tags.label_tag }}
{{ form.tags }}
</div>
<!-- 마크다운 에디터 -->
<h2>contents</h2>
<div class="col-md-12">
{{ form.contents }}
</div>
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Create Post</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit();
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
});
</script>
{% endblock %}

View File

@ -8,8 +8,11 @@
<h5 class="text-muted">{{ post.summary }}</h5>
{% endif %}
<!-- Author -->
<div class="text-muted fst-italic mb-2">{{ post.created_at }} <br>
by. {{ post.author | upper }}</div>
<div class="text-muted fst-italic mb-2">{{ post.created_at }}
<br>
by.
{{ post.author | upper }}</div>
<!-- Rendered Markdown Content -->
<div>
{{ post.render_markdown|safe }}
</div>
@ -22,5 +25,43 @@
<br/>
<br/>
{% endif %}
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary mt-3">Back to List</a>
{% if request.user == post.author %}
<div class="d-flex mt-3">
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary me-2">List</a>
<a href="{% url 'blog:update_post' post.pk %}" class="btn btn-warning me-2">Edit</a>
<form method="POST" action="{% url 'blog:delete_post' post.pk %}" class="d-inline" id="delete-form">
{% csrf_token %}
<button type="button" class="btn btn-danger" id="delete-button">Delete</button>
</form>
</div>
{% endif %}
<!-- Highlight.js Styles -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<!-- Highlight.js Script -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// Initialize Highlight.js
document.addEventListener("DOMContentLoaded", () => {
document
.querySelectorAll("pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
<!-- Delete post -->
<script>
document
.getElementById("delete-button")
.addEventListener("click", function () {
const confirmed = confirm("Are you sure you want to delete this post?");
if (confirmed) {
document
.getElementById("delete-form")
.submit();
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "components/base.html" %}
{% block title %}Post{% endblock %}
{% block main_area %}
<!-- Title -->
<h1 class="mt-4">{{ post.title }}</h1>
{% if post.summary %}
<h5 class="text-muted">{{ post.summary }}</h5>
{% endif %}
<!-- Author -->
<div class="text-muted fst-italic mb-2">{{ post.created_at }}
<br>
by.
{{ post.author | upper }}</div>
<div>
{{ post.render_markdown|safe }}
</div>
<hr>
{% if post.tags.exists %}
<i class="fa-solid fa-tags"></i>
{% for tag in post.tags.all %}
<span class="badge bg-primary">{{ tag }}</span>
{% endfor %}
<br/>
<br/>
{% endif %}
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary mt-3">List</a>
{% if request.user == post.author %}
<a href="{% url 'blog:update_post' post.pk %}" class="btn btn-warning">Edit</a>
{% endif %}
{% endblock %}

View File

@ -12,7 +12,7 @@
<li class="list-group-item">
<h5>{{ post.title }}</h5>
<p>{{ post.summary }}</p>
<a href="{% url 'blog:post_detail' post.pk %}" class="btn btn-secondary">Read More</a>
<a href="{{ post.get_absolute_url }}" class="btn btn-secondary">Read More</a>
</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,58 @@
{% extends "components/base.html" %}
{% block title %}Update Post{% endblock %}
{% block main_area %}
<h1 class="pt-3">Update Post</h1>
<div class="container">
<div class="row">
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
</div>
<div class="mb-3">
{{ form.summary.label_tag }}
{{ form.summary }}
</div>
<div class="mb-3">
{{ form.tags.label_tag }}
{{ form.tags }}
</div>
<!-- 마크다운 에디터 -->
<h2>Contents</h2>
<div class="col-md-12">
{{ form.contents }}
</div>
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Save Changes</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit();
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
});
</script>
</div>
{% endblock %}

View File

@ -7,4 +7,6 @@ urlpatterns = [
path('', views.post_list, name='post_list'), # 포스트 리스트
path('create/', views.create_post, name='create_post'), # 포스트 작성
path('<int:pk>/', views.post_detail, name='post_detail'), # 포스트 상세 보기
path('<int:pk>/update/', views.update_post, name='update_post'), # 글 수정 URL
path('<int:pk>/delete/', views.delete_post, name='delete_post'), # 삭제 URL
]

View File

@ -3,6 +3,16 @@ from django.contrib.auth.decorators import login_required
from .models import Post
from .forms import PostForm
# 게시글 목록
def post_list(request):
posts = Post.objects.all().order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})
# 게시글 상세 보기
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/post_detail.html', {'post': post})
@login_required
def create_post(request):
if request.method == 'POST':
@ -17,10 +27,37 @@ def create_post(request):
form = PostForm()
return render(request, 'blog/create_post.html', {'form': form})
def post_list(request):
posts = Post.objects.all().order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})
def post_detail(request, pk):
@login_required
def update_post(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/post_detail.html', {'post': post})
# 작성자만 수정 가능하도록 권한 검사
if post.author != request.user:
return redirect('blog:post_list')
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/update_post.html', {'form': form, 'post': post})
@login_required
def delete_post(request, pk):
post = get_object_or_404(Post, pk=pk)
# 작성자만 삭제 가능
if post.author != request.user:
return redirect('blog:post_list')
# POST 요청일 때 삭제
if request.method == 'POST':
post.delete()
return redirect('blog:post_list')
return render(request, 'blog/confirm_delete.html', {'post': post})