from django.db import models
class TimestampModel(models.Model):
created_at = models.DateTimeField('생성일자',auto_now_add=True)
updated_at = models.DateTimeField('작성일자', auto_now=True)
class Meta:
abstract = True # 종속되는 것이기에 abstract
from django.contrib.auth import get_user_model
from django.db import models
from utils.models import TimestampModel
User = get_user_model()
class Post(TimestampModel):
content = models.TextField('본문')
user = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return f'{self.user} post' # User의 __str__을 nickname 으로 지정해놔서 self.user 는 닉네임 호출
class Meta:
verbose_name = '포스트'
verbose_name_plural = '포스트 목록'
class PostImage(TimestampModel):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
image = models.ImageField('이미지', upload_to='post/%Y/%m/%d')
def __str__(self):
return f'{self.post} image'
class Meta:
verbose_name ='이미지'
verbose_name_plural='이미지 목록'
# post/admin.py
from django.contrib import admin
from post.models import PostImange, Post
class PostImageInline(admin.TabularInline): # post 안에 들어가야 하니까 inline
model = PostImage
fields=['image']
extra = 1
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
inlines = [
PostImageInline,
]
# member/admin.py
from django.contrib import admin
from member.forms import User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
...
# settings.py
# Media
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# urls.py
from django.conf.urls.static import static
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# post/views.py
from django.views.generic import ListView
from post.models import Post
class PostListView(ListView):
queryset = Post.objects.all().select_related('user')
template_name = 'post/list.html'
paginate_by = 20
ordering = ['created_at']
# config/urls.py
path('', post_views.PostListView.as_View(), name='main'),
<!-- post/list.html -->
{% extends 'base.html' %}
{% block content %}
<div>
</div>
{% endblock %}
<!-- post/list.html -->
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="col-10 offset-1 col-lg-6 offset-3">
{% for post in object_list %}
<!-- PostImage 모델에 related_name images 라 적어서 아래와 같이 사용 가능 아니면 post.postimage_set 을써야함 -->
{% for image in post.images.all %}
<div class="border-1">
<img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}
<!-- base.html -->
{% block style %}{% endblock %}
</head>
<script src="{% static 'js/bootstrap.bundle.js' %}"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- </body> 바로위에 이부분 추가 -->
{% block js %}{% endblock %}
</body>
<!-- post/list.html 부분 위아래 추가 -->
{% extends 'base.html' %}
<!-- 이부분 추가 -->
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-10 offset-1 col-lg-6 offset-3">
{% for post in object_list %}
<div class="border-1">
<img class="img-fluid" src="{{ post.images.first.image.url }}" alt="">
</div>
{% endfor %}
</div>
</div>
{% endblock %}
<!-- 아래에 이부분 추가 -->
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
{% endblock %}
<!-- post/list.html -->
{% extends 'base.html' %}
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
<style>
.post-image{
aspect-ratio: 1 / 1;
object-fit: cover;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-10 offset-1 col-lg-6 offset-lg-3">
{% for post in object_list %}
<div class="swiper" style="max-height: 500px;"> <!-- 메인 swiper -->
<div class="border-1 swiper-wrapper"> <!-- 이미지가 여러개 있을거니까 여기에 wrapper -->
{% for image in post.images.all %}
<div class="swiper-slide">
<img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<!-- swiper Initialize Swiper 부분 가져오기 -->
<script>
const swiper = new Swiper('.swiper', {
// Optional parameters
direction: 'horizontal',
loop: false,
// If we need pagination
pagination: {
el: '.swiper-pagination',
},
});
</script>
{% endblock %}
<i class="fa-solid fa-user"></i>
이 코드 넣어서 아이콘 모양 쓰기 -> content 나오게 하는데 텍스트의 줄넘김 처리해주는 문법 사용 {{ post.content | linebreaksbr }}
-><!-- base.html -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- font awesome 적용 -->
<link href="{% static 'font-awesome/css/fontawesome.css' %}" rel="stylesheet" />
<link href="{% static 'font-awesome/css/custom-icons.css' %}" rel="stylesheet"/>
<link href="{% static 'font-awesome/css/sharp-solid.css' %}" rel="stylesheet" />
<!-- list.html -->
{% extends 'base.html' %}
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
<style>
.post-image{
aspect-ratio: 1 / 1;
object-fit: cover;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-10 offset-1 col-lg-6 offset-lg-3">
{% for post in object_list %}
<div class="border-bottom my-4">
<div class="mb-2">
<span class="p-2 border rounded-circle me-2">
<i class="fa-solid fa-user" style="width: 16px; padding-left: 1px;"></i>
</span>
<!-- 작성자 -->
{{ post.user.nickname }}
</div>
<div class="swiper" style="max-height: 500px;"> <!-- 메인 swiper -->
<div class="border-1 swiper-wrapper"> <!-- 이미지가 여러개 있을거니까 여기에 wrapper -->
{% for image in post.images.all %}
<div class="swiper-slide">
<img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
<div>
{# TODO : like #}
</div>
<div class="my-2">
{{ post.content | linebreaksbr }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<!-- swiper Initialize Swiper 부분 가져오기 -->
<script>
const swiper = new Swiper('.swiper', {
// Optional parameters
direction: 'horizontal',
loop: false,
// If we need pagination
pagination: {
el: '.swiper-pagination',
},
});
</script>
{% endblock %}
queryset = Post.objects.all().select_related('user').prefetch_related('images')
# post 가 foreginkey 를 가지고 있으면 select_related 가능 (join)
# 역참조 일때 prefetch_related (파이썬에서 알아서 가져오는것)
. waypoint js 검색 -> imakewebthings/waypoints 깃 접속 -> 내려서 shortcuts에 infinite scroll 접속 -> About -> 다운 -> 다시 돌아가서 인피니티 스크롤에서 필요한 2가지 파일을 확인 -> lib 에서 찾아서 static 안에 waypoints 라는 폴더 생성 후 폴더안에 넣기 -> list 페이지에서 무한스크롤 제작 -> script 넣기 -> 상단에 {% load static %} -> swiper 밑에 let infinite = new Waypoint.Infinite({ element: $('.infinite-container')[0] })
이부분 붙여넣기 -> infinite 컨테이너 넣어주기
2. shell로 포스트 여러개 늘리기
post_list = Post.objects.all()
for _ in range(20):
for post in post_list:
images=PostImage.objects.filter(post=post)
post.id=None
post.save()
for image in images:
image.id = None
image.post = post
image.save()
<scirpt>
추가 -> jquery란 자바스크립트를 좀더 간단하게 쓸수있게 해주는 패키지offset: 'bottom-in-view'
let infinite 설정 element 밑에 추가{% extends 'base.html' %}
{% load static %}
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
<style>
.post-image{
aspect-ratio: 1 / 1;
object-fit: cover;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-10 offset-1 col-lg-6 offset-lg-3 infinite-container">
{% for post in object_list %}
<div class="border-bottom my-4 infinite-item">
<div class="mb-2">
<span class="p-2 border rounded-circle me-2">
<i class="fa-solid fa-user" style="width: 16px; padding-left: 1px;"></i>
</span>
<!-- 작성자 -->
{{ post.user.nickname }} {{ post.id }}
</div>
<div class="swiper" style="max-height: 500px;"> <!-- 메인 swiper -->
<div class="border-1 swiper-wrapper"> <!-- 이미지가 여러개 있을거니까 여기에 wrapper -->
{% for image in post.images.all %}
<div class="swiper-slide">
<img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
<div>
{# TODO : like #}
</div>
<div class="my-2">
{{ post.content | linebreaksbr }}
</div>
</div>
{% endfor %}
<!-- has_next : 다음페이지가 있는지 확인하는 기능 -->
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="infinite-more-link d-none"></a>
{% endif %}
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'waypoints/jquery.waypoints.min.js' %}"></script>
<script src="{% static 'waypoints/infinite.min.js' %}"></script>
<!-- swiper Initialize Swiper 부분 가져오기 -->
<script>
const swiper = new Swiper('.swiper', {
// Optional parameters
direction: 'horizontal',
loop: false,
// If we need pagination
pagination: {
el: '.swiper-pagination',
},
});
let infinite = new Waypoint.Infinite({
element: $('.infinite-container')[0],
offset: 'bottom-in-view'
})
</script>
{% endblock %}
# post/forms.py 제작
from django import forms
from post.models import Post
class PostForm(forms.ModelForm):
class Meta:
model=Post
fields='__all__'
# post/views.py
class PostCreateView(LoginRequiredMixin,CreateView):
model = Post
template_name = 'post/form.html'
form_class = PostForm
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user # 유저는 입력 부분이 없을테니까 지정
self.object.save()
return HttpResponseRedirect(reverse('main'))
# config/urls.py 연결
path('create/', post_views.PostCreateView.as_view(), name= 'create'),
<!-- post/form.html -->
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="col-10 offset-1 col-lg-6 offset-lg-3">
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary">저장</button>
</form>
</div>
</div>
{% endblock %}
# utils/forms.py
from django import forms
class BootstrapModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
if 'class' in field.widget.attrs: # 클래스가 있으면
field.widget.attrs['class'] += ' form-control' # 텍스트로 들어가니까 띄어쓰기 해야함
else:
field.widget.attrs.update({'class': 'form-control'}) # 없으면 클래스를 넣는것
# post/forms.py 에 상속
from post.models import Post
from utils.forms import BootstrapModelForm
class PostForm(BootstrapModelForm):
class Meta:
model=Post
fields=('content',) # 생성시 user 선택은 있으면 안되니까 제외 하고 content 만 설정
{% block js %}
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/jquery.formset.js' %}"></script>
{% endblock %}
# post/forms.py
class PostImageForm(BootstrapModelForm):
class Meta:
model = PostImage
fields=('image',) # 이미지만 있으면 되니까
from django.forms import inlineformset_factory
PostImageFormSet = inlineformset_factory( # 다중 이미지 업로드를 위한 FormSet
# (부모모델)parent model, (자식모델)현재 모델 , 방금만든 포스트이미지 폼, 몇개를 한번에 넣을건지 , 삭제가능여부
Post, PostImage, form=PostImageForm, extra=1, can_delete=True
)
# post/views.py
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
template_name = 'post/form.html'
form_class = PostForm
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) # 기본 context 가져오기
data['formset'] = PostImageFormSet(instance=self.object) # formset 이라는 이름으로 PostImageFormSet을 전달
# instance 현재 수정중인 Post객체와 연결된 Postimage 들을 가져와서
return data
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="row">
<div class="col-10 offset-1 col-lg-6 offset-lg-3">
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<table class="table">
{{ formset.management_form }}
<thead>
<tr>
{% for field in formset.forms.0.visible_fields %}
<th class="text-xs">{{ field.label }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<!-- formset은 PostImageForm 이 리스트 형태로 여러개가 들어가 있는거임 그래서 for문 사용 -->
{% for form in formset.forms %}
<tr class="formset_row">
{% for field in form.visible_fields %}
<td>
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
<!-- 이부분이랑 아랫부분은 form 을 만들어서 쓸때 고정적으로 입력한다 생각 -->
{{ field.errors.as_ul }}
{{ field }}
{% if field.errors %}
{% for error in field.errors %}
<span class="text-danger"></span>
{% endfor %}
{% endif %}
</td>
{% endfor %}
</tr>
<!-- form.as_p애서 자동으로 처리하던것 -->
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="text-danger">{{ error }}</p>
{% endfor %}
{% endif %}
{% endfor %}
</tbody>
</table>
<button class="btn btn-primary">저장</button>
</form>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/jquery.formset.js' %}"></script>
<!-- 버튼추가 -->
<script>
$('.formset_row').formset({
addText: '추가하기',
deleteText: '삭제',
prefix: 'images'
})
</script>
{% endblock %}
# post/views.py PostCreateView
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()
# 이미지가 타당한지 확인하는 것
image_formset = PostImageFormSet(self.request.POST, self.request.FILES, instance=self.object)
if image_formset.is_valid():
image_formset.save()
return HttpResponseRedirect(reverse('main'))
# post/views.py
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
template_name = 'post/form.html'
form_class = PostForm
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) # 기본 context 가져오기
# 인스턴스 설정을 하면 ForeignKey로 묶여있는 데이터를 같이 가져와줌
data['formset'] = PostImageFormSet(instance=self.object) # 어떤 것의 formset인지만 넣어줌
return data
def form_valid(self, form):
self.object.save()
image_formset = PostImageFormSet(self.request.POST, self.request.FILES, instance=self.object)
if image_formset.is_valid():
image_formset.save()
return HttpResponseRedirect(reverse('main'))
# 현재 글 작성유저와 같은 사람만 와야하니
def get_queryset(self):
queryset=super().get_queryset()
return queryset.filter(user=self.request.user)
# config/urls.py
path('<int:pk>/update/', post_views.PostUpdateView.as_view(), name='update'),
{% block content %}
<div class="row">
<div class="text-end col-10 offset-1 col-lg-6 offset-lg-3">
<a class="btn btn-sm btn-info" href="{% url 'create' %}">생성</a>
</div>
<!-- 작성자 -->
{{ post.user.nickname }}
{% if post.user == request.user %}
<a href="{% url 'update' post.pk %}" class="btn btn-warning btn-sm float-end">
수정
</a>
{% endif %}
# 태그
class Tag(TimestampModel):
tag = models.CharField('태그', max_length=100)
# N대 N 관계 ManyToManyField
posts = models.ManyToManyField(Post, related_name='tags')
# many to many 를 이렇게 풀어서 쓰는 경우도 있음
# class TagPosts(TimestampModel):
# tag=models.ForeginKey(Tag)
# post=models.ForeignKey(Post)
# 댓글
class Comment(TimestampModel):
post=models.ForeignKey(Post, related_name='comments', on_delete=models.CASCADE)
content = models.CharField('내용', max_length=255)
user = models.ForeignKey(User, on_delete=models.CASCADE)
post
[X] 0001_initial
[X] 0002_alter_postimage_post_comment_tag
sessions
[X] 0001_initial
post
[X] 0001_initial
[ ] 0002_alter_postimage_post_comment_tag
select_related
vs prefetch_related
비교특징 | select_related | prefetch_related |
---|---|---|
사용 대상 | ForeignKey , OneToOneField (1:N, 1:1) | ManyToManyField , 역참조 (N:N, 1:N 역참조) |
쿼리 방식 | JOIN 을 사용하여 한 번의 SQL 쿼리로 가져옴 | 여러 개의 쿼리를 실행한 후, Python이 관계를 매칭 |
성능 | JOIN 을 사용하기 때문에 SQL 실행이 빠름 (적은 데이터에 유리) | 여러 개의 개별 쿼리를 실행하기 때문에 대량 데이터에 유리 |
추가 쿼리 발생 여부 | 관련 객체 접근 시 추가 쿼리 발생하지 않음 | 관련 객체 접근 시 추가 쿼리 발생하지 않음 |
예제 | Post.objects.select_related('author').all() | Post.objects.prefetch_related('comments').all() |
select_related
사용Post.objects.select_related('author').all() # ✅ Post와 User를 JOIN
prefetch_related
사용Post.objects.prefetch_related('comments').all() # ✅ Post와 Comment를 별도 쿼리 후 조합
select_related
사용 (JOIN
활용, 즉시 로드).prefetch_related
사용 (IN
활용, Python에서 로드).