728x90
1.3 Template 중복코드 제거
1.3.1 화면 단위 기능 정리
- 지금까지 만든 화면 단위 기능
- 로그인
- 회원가입
- 피드 페이지
- 태그 페이지
- 글 상세 페이지
- 글 작성 페이지
- 비슷한 레이아웃을 가진 기능 묶음
- 상단 내비게이션 바가 없는 레이아웃
- 로그인
- 회원가입
- 내비게이션 바가 있는 레이아웃
- 이미지 슬라이더 기능이 필요한 레이아웃
- 피드 페이지
- 글 상세 페이지
- 이미지 슬라이더가 없어도 되는 레이아웃
- 글 작성 페이지
- 태그 페이지
- 이미지 슬라이더 기능이 필요한 레이아웃
- 상단 내비게이션 바가 없는 레이아웃
- 레이아웃에 따라 base 정리
- 상단 내비게이션 바가 없는 레이아웃: base.html
- 내비게이션 바가 있는 레이아웃: base_nav.html
- 내비게이션 바가 있으며 이미지 슬라이더 기능이 포함된 레이아웃: base_slider.html
1.3.2 base.html 분할
- templates/_base.html
- 모든 기반 레이아웃의 최상단 Template
{% load static %}
<!doctype html>
<html lang="ko">
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<title>Pystagram</title>
{% block head %}{% endblock %}
</head>
<body>
{% block base_content %}{% endblock %}
</body>
</html>
|
- templates/base.html
- 로그인, 회원가입에서 사용
{% extends '_base.html' %}
{% block base_content %}
{% block content %}{% endblock %}
{% endblock %}
|
- templates/base_nav.html
- 글 작성에서 사용
{% extends '_base.html' %}
{% block base_content %}
{% include 'nav.html' %}
{% block content %}{% endblock %}
{% endblock %}
|
- templates/base_slider.html
- 피드, 글 상세에서 사용
{% extends '_base.html' %}
{% load static %}
{% block head %}
<link href="{% static 'splide/splide.css' %}" rel="stylesheet">
<script src="{% static 'splide/splide.js' %}"></script>
{% endblock %}
{% block base_content %}
{% include 'nav.html' %}
{% block content %}{% endblock %}
<script>
const elms = document.getElementsByClassName('splide');
for (let i = 0; i < elms.length; i++) {
new Splide(elms[i]).mount();
}
</script>
{% endblock %}
|
1.3.3 분할한 Template을 사용하도록 코드 수정
- templates/posts/feeds.html
{% extends 'base_slider.html' %}
{% load custom_tags %}
{% block content %}
<div id="feeds" class="post-container">
{% for post in posts %}
{% url 'posts:feeds' as action_redirect_to %}
{% include 'posts/post.html' with action_redirect_url=action_redirect_to|concat:'#post-'|concat:post_id %}
{% endfor %}
</div>
{% endblock %}
|
- templates/posts/post_detail.html
{% extends 'base_slider.html' %}
{% block content %}
<div id="feeds" class="post-container">
{% url 'posts:post_detail' post.id as action_redirect_to %}
{% include 'posts/post.html' with action_redirect_url=action_redirect_to %}
</div>
{% endblock %}
|
- templates/posts/tags.html
{% extends 'base_nav.html' %}
{% block content %}
<div id="tags">
...
</div>
{% endblock %}
|
- templates/posts/post_add.html
{% extends 'base_nav.html' %}
{% block content %}
<div id="post-add">
...
</div>
{% endblock %}
|
2. 좋아요 기능
2.1 좋아요 모델, 관리자 구성
2.1.1 ManyToManyField 추가
- users/models.py
class User(AbstractUser):
...
like_posts = models.ManyToManyField(
"posts.Post",
verbose_name="좋아요 누른 Post목록",
related_name="like_users",
blank=True,
)
|
- Terminal
python manage.py makemigrations
python manage.py migrate
|
2.1.2 admin 구성
- users/admin.py
...
@admin.register(User)
class CustomUserAdmin(UserAdmin):
fieldsets = [
...
(
"추가필드",
{
"fields": ("profile_image", "short_description"),
},
),
(
"연관객체",
{
"fields": ("like_posts",),
},
),
...
|
- posts/models.py
...
class Post(models.Model):
...
def __str__(self):
return f"{self.user.username}의 Post(id: {self.id})"
...
|
- users/models.py
...
class User(AbstractUser):
...
def __str__(self):
return self.username
...
|
- posts/admin.py
...
class PostImageInline(admin.TabularInline):
...
class LikeUserInline(admin.TabularInline):
model = Post.like_users.through
verbose_name = "좋아요 한 User"
verbose_name_plural = f"{verbose_name} 목록"
extra = 1
def has_change_permission(self, request, obj=None):
return False
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
...
inlines = [
CommentInline,
PostImageInline,
LikeUserInline,
]
...
|
2.2 좋아요 토글 액션
2.2.1 View 구현
- posts/views.py
# URL에서 좋아요 처리할 Post의 id를 전달받는다.
def post_like(request, post_id):
post = Post.objects.get(id=post_id)
user = request.user
# 사용자가 "좋아요를 누른 Post목록"에 "좋아요 버튼을 누른 Post"가 존재한다면
if user.like_posts.filter(id=post.id).exists():
# 좋아요 목록에서 삭제한다
user.like_posts.remove(post)
# 존재하지 않는다면 좋아요 목록에 추가한다.
else:
user.like_posts.add(post)
# next로 값이 전달되었다면 해당 위치로, 전달되지 않았다면 피드페이지에서 해당 Post위치로 이동한다
url_next = request.GET.get("next") or reverse("posts:feeds") + f"#post-{post.id}"
return HttpResponseRedirect(url_next)
|
2.2.2 URLconf
- posts/urls.py
from posts.views import ..., post_like
...
app_name = "posts"
urlpatterns = [
...
path("<int:post_id>/like/", post_like, name="post_like"),
]
|
2.2.3 Template의 좋아요 버튼에 form 추가
- templates/posts/post.html
...
<div class="post-buttons">
<form action="{% url 'posts:post_like' post_id=post.id %}?next={{ action_redirect_url }}" method="POST">
{% csrf_token %}
<button type="submit"
{% if user in post.like_users.all %}
style="color: red;"
{% endif %}>
Likes({{ post.like_users.count }})
</button>
</form>
<span>Comments({{ post.comment_set.count }})</span>
</div>
...
|
3. 팔로우/팔로잉 기능
3.1 팔로우/팔로잉 모델, 관리자 구성
3.1.1 팔로우/팔로잉 관계
- '해시태그', '좋아요'와 마찬가지로 ManyToManyField를 사용한 다대다관계로 구성
- '해시태그', '좋아요'와 다른 점
- '해시태그', '좋아요': 한쪽에서의 연결은 반대쪽에서의 연결도 나타내는 대칭적 관계
- 팔로우/팔로잉 관계: 한 쪽에서의 연결과 반대쪽에서의 연결이 별도로 구분되는 비대칭적 관계
- 같은 테이블(User)에서의 관계를 나타내야 함
- 예시
- User.username = [녹턴, 럭스, 람머스]
- 이 User의 팔로워들(Followers)
- 녹턴의 팔로워들: 람머스
- 럭스의 팔로워들: 녹턴, 람머스
- 람머스의 팔로워들: 없음
- 이 User가 팔로잉하는 대상들(Folowing)
- 녹턴이 팔로잉하는 사용자들: 럭스
- 럭스가 팔로잉하는 사용자들: 없음
- 람머스가 팔로잉하는 사용자들: 녹턴, 럭스
- 팔로우/팔로잉 관계를 구성하는 중개 테이블
- 이 중개 테이블의 데이터는 방향에 따라 나타내는 관계가 다른 비대칭적 관계를 나타냄
- From User의 사용자는 To User의 사용자를 팔로우
- To User의 사용자에게 From User의 사용자는 자신을 팔로잉하는 사용자로 취급
3.1.2 팔로우 관계 모델
- users/models.py
class Relationship(models.Model):
from_user = models.ForeignKey(
"users.User",
verbose_name="팔로우를 요청한 사용자",
related_name="following_relationships",
on_delete=models.CASCADE,
)
to_user = models.ForeignKey(
"users.User",
verbose_name="팔로우 요청의 대상",
related_name="follower_relationships",
on_delete=models.CASCADE,
)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"관계 ({self.from_user} -> {self.to_user})"
|
...
class User(AbstractUser):
...
following = models.ManyToManyField(
"self",
verbose_name="팔로우 중인 사용자들",
related_name="followers",
symmetrical=False,
through="users.Relationship",
)
...
|
- Terminal
python manage.py makemigrations
python manage.py migrate
|
3.1.3 팔로우 관계 admin
- users/admin.py
...
class FollowersInline(admin.TabularInline):
model = User.following.through
fk_name = "from_user"
verbose_name = "내가 팔로우 하고 있는 사용자"
verbose_name_plural = f"{verbose_name} 목록"
class FollowingInline(admin.TabularInline):
model = User.following.through
fk_name = "to_user"
verbose_name = "나를 팔로우 하고 있는 사용자"
verbose_name_plural = f"{verbose_name} 목록"
@admin.register(User)
class CustomUserAdmin(UserAdmin):
fieldsets = [
...
]
inlines = [
FollowersInline,
FollowingInline,
]
|
3.2 프로필 페이지
3.2.1 프로필 페이지 기본구조 구성 및 연결
- View: users/views.py → profile
- URL: /users/int:user_id/profile/
- Template: templates/users/profile.html
- users/views.py
def profile(request, user_id):
return render(request, "users/profile.html")
|
- users/urls.py
from users.views import ..., profile
...
app_name = "users"
urlpatterns = [
...
path("<int:user_id>/profile/", profile, name="profile"),
...
]
|
- templates/users/profile.html
{% extends 'base_nav.html' %}
{% block content %}
<div id="profile">
<h1>Profile</h1>
</div>
{% endblock %}
|
- templates/posts/post.html
<article id="post-{{ post.id }}" class="post">
<header class="post-header">
<a href="{% url 'users:profile' user_id=post.user.id %}">
{% if post.user.profile_image %}
<img src="{{ post.user.profile_image.url }}" alt="">
{% endif %}
<span>{{ post.user.username }}</span>
</a>
</header>
...
|
3.2.2 프로필 Template에 정보 전달
- users/views.py
from django.shortcuts import render, redirect, get_object_or_404
...
from users.models import User
def profile(request, user_id):
user = get_object_or_404(User, id=user_id)
context = {
"user": user,
}
return render(request, "users/profile.html", context)
|
3.2.3 프로필 Template 구성
- templates/users/profile.html
{% extends 'base_nav.html' %}
{% block content %}
<div id="profile">
<div class="info">
<!-- 프로필 이미지 영역 -->
{% if user.profile_image %}
<img src="{{ user.profile_image.url }}">
{% endif %}
<!-- 사용자 정보 영역 -->
<div class="info-texts">
<h1>{{ user.username }}</h1>
<div class="counts">
<dl>
<dt>Posts</dt>
<dd>{{ user.post_set.count }}</dd>
<dt>Followers</dt>
<dd>{{ user.followers.count }}</dd>
<dt>Following</dt>
<dd>{{ user.following.count }}</dd>
</dl>
</div>
<p>{{ user.short_description }}</p>
</div>
</div>
<!-- 사용자가 작성한 Post목록 -->
<div class="post-grid-container">
{% for post in user.post_set.all %}
{% if post.postimage_set.first %}
{% if post.postimage_set.first.photo %}
<div class="post-grid">
<a href="{% url 'posts:post_detail' post_id=post.id %}">
<img src="{{ post.postimage_set.first.photo.url }}" alt="">
</a>
</div>
{% endif %}
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
|
3.3 팔로우/팔로잉 목록
3.3.1 중개 테이블의 데이터 가져오기
- Terminal
python manage.py shell
from users.models import User, Relationship
user = User.objects.get(id=1)
user.followers.all()
user.follower_relationships.all()
for relationship in user.follower_relationships.all():
print(relationship, relationship.created)
|
3.3.2 base_profile.html 구성
- templates/base_profile.html
{% extends 'base_nav.html' %}
{% block content %}
<div id="profile">
<div class="info">
<!-- 프로필 이미지 영역 -->
{% if user.profile_image %}
<img src="{{ user.profile_image.url }}">
{% endif %}
<!-- 사용자 정보 영역 -->
<div class="info-texts">
<h1>{{ user.username }}</h1>
<div class="counts">
<dl>
<dt>Posts</dt>
<dd>{{ user.post_set.count }}</dd>
<dt>Followers</dt>
<dd>{{ user.followers.count }}</dd>
<dt>Following</dt>
<dd>{{ user.following.count }}</dd>
</dl>
</div>
<p>{{ user.short_description }}</p>
</div>
</div>
{% block bottom_data %}{% endblock %}
</div>
{% endblock %}
|
- templates/users/profile.html
{% extends 'base_profile.html' %}
{% block bottom_data %}
<!-- 사용자가 작성한 Post목록 -->
<div class="post-grid-container">
{% for post in user.post_set.all %}
{% if post.postimage_set.first %}
{% if post.postimage_set.first.photo %}
<div class="post-grid">
<a href="{% url 'posts:post_detail' post_id=post.id %}">
<img src="{{ post.postimage_set.first.photo.url }}" alt="">
</a>
</div>
{% endif %}
{% endif %}
{% endfor %}
</div>
{% endblock %}
|
3.3.3 팔로우/팔로잉 목록
- 자신을 팔로우하는 사용자 목록(Followers)
- View: users/views.py → followers
- URL: /users/int:user_id/followers/
- Template: templates/users/followers.html
- 자신이 팔로우하는 사용자 목록(Following)
- View: users/views.py → following
- URL: /users/int:user_id/following/
- Template: templates/users/following.html
- users/views.py
...
def followers(request, user_id):
user = get_object_or_404(User, id=user_id)
relationships = user.follower_relationships.all()
context = {
"user": user,
"relationships": relationships,
}
return render(request, "users/followers.html", context)
def following(request, user_id):
user = get_object_or_404(User, id=user_id)
relationships = user.following_relationships.all()
context = {
"user": user,
"relationships": relationships,
}
return render(request, "users/following.html", context)
...
|
- users/urls.py
from users.views import ..., followers, following
...
app_name = "users"
urlpatterns = [
...
path("<int:user_id>/followers/", followers, name="followers"),
path("<int:user_id>/following/", following, name="following"),
]
|
- 실질적으로 위에까지 진행 아래는 다음주 월요일 예정
- templates/users/followers.html
{% extends 'base_profile.html' %}
{% block bottom_data %}
<div class="relationships">
<h3>Followers</h3>
{% for relationship in relationships %}
<div class="relationship">
<a href="{% url 'users:profile' user_id=relationship.from_user.id %}">
{% if relationship.from_user.profile_image %}
<img src="{{ relationship.from_user.profile_image.url }}">
{% endif %}
<div class="relationship-info">
<span>{{ relationship.from_user.username }}</span>
<span>{{ relationship.created|date:"y.m.d" }}</span>
</div>
</a>
</div>
{% endfor %}
</div>
{% endblock %}
|
- templates/users/following.html
{% extends 'base_profile.html' %}
{% block bottom_data %}
<div class="relationships">
<h3>Following</h3>
{% for relationship in relationships %}
<div class="relationship">
<a href="{% url 'users:profile' user_id=relationship.to_user.id %}">
{% if relationship.to_user.profile_image %}
<img src="{{ relationship.to_user.profile_image.url }}">
{% endif %}
<div class="relationship-info">
<span>{{ relationship.to_user.username }}</span>
<span>{{ relationship.created|date:"y.m.d" }}</span>
</div>
</a>
</div>
{% endfor %}
</div>
{% endblock %}
|
3.3.4 프로필 페이지 링크 구성
- templates/base_profile.html
...
<!-- 사용자 정보 영역 -->
<div class="info-texts">
<h1>{{ user.username }}</h1>
<div class="counts">
<dl>
<dt>Posts</dt>
<dd>
<a href="{% url 'users:profile' user_id=user.id %}">{{ user.post_set.count }}</a>
</dd>
<dt>Followers</dt>
<dd>
<a href="{% url 'users:followers' user_id=user.id %}">{{ user.followers.count }}</a>
</dd>
<dt>Following</dt>
<dd>
<a href="{% url 'users:following' user_id=user.id %}">{{ user.following.count }}</a>
</dd>
</dl>
</div>
...
|
3.4 팔로우 버튼
3.4.1 팔로우 토글 View
- View: users/views.py → follow
- URL: /users/int:user_id/follow/
- Template: 없음
- users/views.py
from django.http import HttpResponseRedirect
from django.urls import reverse
...
def follow(request, user_id):
# 로그인 한 유저
user = request.user
# 팔로우 하려는 유저
target_user = get_object_or_404(User, id=user_id)
# 팔로우 하려는 유저가 이미 자신의 팔로잉 목록에 있는 경우
if target_user in user.following.all():
# 팔로잉 목록에서 제거
user.following.remove(target_user)
# 팔로우 하려는 유저가 자신의 팔로잉 목록에 없는 경우
else:
# 팔로잉 목록에 추가
user.following.add(target_user)
# 팔로우 토글 후 이동할 URL이 전달되었다면 해당 주소로,
# 전달되지 않았다면 로그인 한 유저의 프로필 페이지로 이동
url_next = request.GET.get("next") or reverse("users:profile", args=[user.id])
return HttpResponseRedirect(url_next)
|
- users/urls.py
from users.views import ..., follow
...
app_name = "users"
urlpatterns = [
...
path("<int:user_id>/follow/", follow, name="follow"),
]
|
3.4.2 팔로우 버튼 추가
- templates/posts/post.html
<article id="post-{{ post.id }}" class="post">
<header class="post-header">
<a href="{% url 'users:profile' user_id=post.user.id %}">
...
</a>
<!-- 글의 작성자가 로그인 한 사용자라면 팔로우 버튼을 표시하지 않는다 -->
<!-- (자기 자신을 팔로우 하는것을 방지) -->
{% if user != post.user %}
<form action="{% url 'users:follow' user_id=post.user.id %}?next={{ action_redirect_url }}" method="POST">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
<!-- 이 Post의 작성자가 이미 자신의 팔로잉 목록에 포함된 경우 -->
{% if post.user in user.following.all %}
Unfollow
<!-- 이 Post의 작성자를 아직 팔로잉 하지 않은 경우 -->
{% else %}
Follow
{% endif %}
</button>
</form>
{% endif %}
</header>
...
|
728x90
'PYTHON-BACK' 카테고리의 다른 글
#파이썬 32일차_온라인 상점 구축 프로젝트2 (0) | 2024.08.26 |
---|---|
#파이썬 32일차_온라인 상점 구축 프로젝트1 (0) | 2024.08.23 |
#파이썬 34일차_이커머스 클론코딩5_(1) (0) | 2024.08.22 |
#파이썬 34일차_이커머스 클론코딩4_(2) (0) | 2024.08.21 |
#파이썬 34일차_이커머스 클론코딩4_(1) (0) | 2024.08.21 |