This commit is contained in:
0
blog/__init__.py
Normal file
0
blog/__init__.py
Normal file
4
blog/admin.py
Normal file
4
blog/admin.py
Normal file
@ -0,0 +1,4 @@
|
||||
from django.contrib import admin
|
||||
from .models import Post
|
||||
|
||||
admin.site.register(Post)
|
6
blog/apps.py
Normal file
6
blog/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'blog'
|
14
blog/forms copy.py
Normal file
14
blog/forms copy.py
Normal file
@ -0,0 +1,14 @@
|
||||
from django import forms
|
||||
from .models import Post
|
||||
|
||||
class PostForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Post
|
||||
# fields = ['title', 'content', 'summary', 'tags', 'image']
|
||||
fields = ['title', 'content', 'summary', 'tags']
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'summary': forms.Textarea(attrs={'class': 'form-control'}),
|
||||
'content': forms.Textarea(attrs={'class': 'form-control', 'id': 'id_content'}),
|
||||
'tags': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
}
|
14
blog/forms.py
Normal file
14
blog/forms.py
Normal file
@ -0,0 +1,14 @@
|
||||
from django import forms
|
||||
from taggit.forms import TagWidget
|
||||
from .models import Post
|
||||
|
||||
class PostForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = ['title', 'content', 'summary', 'tags']
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'content': forms.Textarea(attrs={'class': 'form-control', 'id': 'markdown-editor'}),
|
||||
'summary': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'tags': TagWidget(attrs={'class': 'form-control', 'placeholder': 'Add tags'}),
|
||||
}
|
32
blog/migrations/0001_initial.py
Normal file
32
blog/migrations/0001_initial.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.2.14 on 2024-12-03 22:16
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Post',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('content', models.TextField()),
|
||||
('summary', models.TextField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
|
||||
],
|
||||
),
|
||||
]
|
22
blog/migrations/0002_remove_post_tags_post_tags.py
Normal file
22
blog/migrations/0002_remove_post_tags_post_tags.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.14 on 2024-12-03 22:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='post',
|
||||
name='tags',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='tags',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
18
blog/migrations/0003_alter_post_summary.py
Normal file
18
blog/migrations/0003_alter_post_summary.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.14 on 2024-12-03 23:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0002_remove_post_tags_post_tags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='summary',
|
||||
field=models.CharField(blank=True, max_length=2000, null=True),
|
||||
),
|
||||
]
|
24
blog/migrations/0004_remove_post_tags_post_tags.py
Normal file
24
blog/migrations/0004_remove_post_tags_post_tags.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.14 on 2024-12-04 22:46
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
|
||||
('blog', '0003_alter_post_summary'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='post',
|
||||
name='tags',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
0
blog/migrations/__init__.py
Normal file
0
blog/migrations/__init__.py
Normal file
21
blog/models.py
Normal file
21
blog/models.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from markdown_it import MarkdownIt
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
class Post(models.Model):
|
||||
title = models.CharField(max_length=200) # 제목
|
||||
content = models.TextField() # 본문 (마크다운 저장)
|
||||
summary = models.CharField(max_length=2000, blank=True, null=True) # 요약
|
||||
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)
|
||||
|
||||
def render_markdown(self):
|
||||
"""마크다운을 HTML로 변환"""
|
||||
md = MarkdownIt()
|
||||
return md.render(self.content)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
39
blog/templates/blog/create_post copy.html
Normal file
39
blog/templates/blog/create_post copy.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% 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 %}
|
58
blog/templates/blog/create_post.html
Normal file
58
blog/templates/blog/create_post.html
Normal 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.content }}
|
||||
</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 %}
|
26
blog/templates/blog/post_detail.html
Normal file
26
blog/templates/blog/post_detail.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% 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">Back to List</a>
|
||||
{% endblock %}
|
19
blog/templates/blog/post_list.html
Normal file
19
blog/templates/blog/post_list.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "components/base.html" %}
|
||||
|
||||
{% block title %}Post{% endblock %}
|
||||
|
||||
{% block main_area %}
|
||||
<h1 class="pt-3">Blog Posts</h1>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="{% url 'blog:create_post' %}" class="btn btn-primary mb-3">Create New Post</a>
|
||||
{% endif %}
|
||||
<ul class="list-group">
|
||||
{% for post in posts %}
|
||||
<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>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
3
blog/tests.py
Normal file
3
blog/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
10
blog/urls.py
Normal file
10
blog/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'blog'
|
||||
|
||||
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'), # 포스트 상세 보기
|
||||
]
|
26
blog/views.py
Normal file
26
blog/views.py
Normal file
@ -0,0 +1,26 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .models import Post
|
||||
from .forms import PostForm
|
||||
|
||||
@login_required
|
||||
def create_post(request):
|
||||
if request.method == 'POST':
|
||||
form = PostForm(request.POST)
|
||||
if form.is_valid():
|
||||
post = form.save(commit=False)
|
||||
post.author = request.user # 작성자 정보 추가
|
||||
post.save()
|
||||
form.save_m2m()
|
||||
return redirect('blog:post_list')
|
||||
else:
|
||||
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):
|
||||
post = get_object_or_404(Post, pk=pk)
|
||||
return render(request, 'blog/post_detail.html', {'post': post})
|
Reference in New Issue
Block a user