def get_board_set_from_board_group(request, board_group_id):
...완료!!!
def home(request):
...완료!!!
def board(request, board_url):
...완료!!!
def post_detail(request, board_url, pk):
...이번장!!!
def reply_write(request, board_url, pk):
...
def rereply_write(request, board_url, pk):
...
def reply_delete(request, board_url, pk):
...
def rereply_delete(request, board_url, pk):
...
def like(request, board_url, pk):
...
[ get_board_set_from_board_group 함수 리팩토링 ]
get_board_set_from_board_group 리팩토링 정보 보기
[ home 함수 리팩토링 ]
home 페이지 성능 개선 Board View 코드 리팩토링
home 페이지 좋아요, 댓글 수 Board View 코드 리팩토링
[ board 함수 리팩토링 ]
board 함수 리팩토링 설계
board 함수 리팩토링 일반 게시판 View 재구성
board 함수 리팩토링 태그 게시판 View 재구성
board 함수 리팩토링 전체 게시판 View 재구성
board 함수 리팩토링 게시판 View 불필요 코드 제거
Board 앱의 View 함수들을 살펴보니 총 9개의 코드가 존재합니다.
하나하나씩 불필요한 코드를 제거하거나 리팩토링해 보겠습니다
def post_detail(request, board_url, pk):
qs = Post.objects.active().filter(
board__url=board_url
).select_related(
'board'
).order_by(
'-id'
)
prev_post = qs.filter(id__lt=pk).first()
next_post = qs.filter(id__gt=pk).order_by('id').first()
post = get_object_or_404(qs, board__url=board_url, pk=pk)
post_summary = get_latest_post_summary_by_post_id(post.id)
if request.user.is_authenticated:
like_check = Like.objects.filter(author=request.user, post=post).exists()
else:
like_check = False
context = {
'like_check': like_check,
'qs': qs,
'post': post,
'post_summary': post_summary,
'prev_post': prev_post,
'next_post': next_post,
}
return render(request, 'board/post.html', context)
지금 View 에서 HTML 넘기고 데이터를 보면 like_check, qs, post, post_summary, prev_post, next_post 정보를 넘기고 있습니다.
각각에 대한 부분을 차근차근 정리해서 View 에서 모든 데이터를 전달할 수 있도록 리팩토링해봅시다.
<div class="text-center my-3">
<a id="like" href="{% url 'board:like' post.board.url post.id %}"
class="d-inline text-center border p-2 rounded text-decoration-none text-dark fw-bolder">
{% if like_check %}
<i class="bi bi-heart-fill text-danger"></i>
{% else %}
<i class="bi bi-heart text-danger"></i>
{% endif %}
좋아요
</a>
</div>
like_check 를 사용하는 부분만 보면 아래 사진에 있는 좋아요 옆에 있는 하트 아이콘에 빨간색이 채워지는지 아닌지의 차이입니다.
if request.user.is_authenticated:
like_check = Like.objects.filter(author=request.user, post=post).exists()
else:
like_check = False
context = {
'like_check': like_check,
'qs': qs,
'post': post,
'post_summary': post_summary,
'prev_post': prev_post,
'next_post': next_post,
}
View 코드의 context 부분만 봤을 때, 저는 이게 dictionary 로 되어있는 줄 알았습니다. 물론 위에 있는 코드를 좀 더 읽었으면 알았겠지만 누가봐도 코드를 쉽게 읽을 수 있게 작성하는 게 좋습니다.
가장 좋은 방식은 context 코드를 Dictionary 가 아닌 DTO 클래스를 하나 정의해서 만들면 어떤 타입들인지 알 수 있게 되어 좋습니다.
하지만 그 리팩토링은 context 내부에 있는 모든 데이터를 정리했을 때, 리팩토링 해봅시다.
그러면 조금 수정 해봅시다.
like_check 라는 변수명으로 boolean 타입인지 판단하기 조금 어려울 것 같습니다.
변수명을 like_checked 혹은 is_liked 로 수정해야할 것 같습니다. 저는 is_liked 로 수정하겠습니다.
if request.user.is_authenticated:
is_liked = Like.objects.filter(author=request.user, post=post).exists()
else:
is_liked = False
context = {
'is_liked': is_liked,
'qs': qs,
'post': post,
'post_summary': post_summary,
'prev_post': prev_post,
'next_post': next_post,
}
<div class="text-center my-3">
<a id="like" href="{% url 'board:like' post.board.url post.id %}"
class="d-inline text-center border p-2 rounded text-decoration-none text-dark fw-bolder">
{% if is_liked %}
<i class="bi bi-heart-fill text-danger"></i>
{% else %}
<i class="bi bi-heart text-danger"></i>
{% endif %}
좋아요
</a>
</div>
또한 저는 아래에 있는 코드를 Service Layer 로 넘길 것입니다.
if request.user.is_authenticated:
is_liked = Like.objects.filter(author=request.user, post=post).exists()
else:
is_liked = False
유저의 정보로 post_ids 값을 넣으면 존재하는 like 를 조회하는 기능을 Service Layer 로 분리 했습니다.
def get_liked_post_ids_by_author_id(author_id: Optional[int], post_ids: List[int]) -> Set[int]:
if not author_id:
return set()
if not post_ids:
return set()
return set(
Like.objects.filter(
author_id=author_id,
post_id__in=post_ids,
).values_list(
'post_id',
flat=True,
)
)
아래와 같이 수정했습니다!
def post_detail(request, board_url, pk):
qs = Post.objects.active().filter(
board__url=board_url
).select_related(
'board'
).order_by(
'-id'
)
prev_post = qs.filter(id__lt=pk).first()
next_post = qs.filter(id__gt=pk).order_by('id').first()
post = get_object_or_404(qs, board__url=board_url, pk=pk)
post_summary = get_latest_post_summary_by_post_id(post.id)
context = {
'is_liked': bool(get_liked_post_ids_by_author_id(request.user.id, [post.id])),
'qs': qs,
'post': post,
'post_summary': post_summary,
'prev_post': prev_post,
'next_post': next_post,
}
return render(request, 'board/post.html', context)
def post_detail(request, board_url, pk):
qs = Post.objects.active().filter(
board__url=board_url
).select_related(
'board'
).order_by(
'-id'
)
prev_post = qs.filter(id__lt=pk).first()
next_post = qs.filter(id__gt=pk).order_by('id').first()
post = get_object_or_404(qs, board__url=board_url, pk=pk)
post_summary = get_latest_post_summary_by_post_id(post.id)
context = {
'is_liked': bool(get_liked_post_ids_by_author_id(request.user.id, [post.id])),
'qs': qs,
'post': post,
'post_summary': post_summary,
'prev_post': prev_post,
'next_post': next_post,
}
return render(request, 'board/post.html', context)
qs 의 이용 범위를 보면, prev_post 와 next_post 그리고 html 부분의 게시글 목록 최신 정보를 가져오기 위한 작업이 있습니다.
<!-- 다른 게시판에 있는 게시글 목록 최신 -->
<div class="card mb-3">
<ul class="list-group">
<li class="list-group-item fw-bolder">{{ post.board.name }} 최신 목록</li>
{% for list_post in qs|slice:'0:5' %}
{% if post.id == list_post.id %}
<a href="{% url 'board:post' list_post.board.url list_post.pk %}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center active">
{% else %}
<a href="{% url 'board:post' list_post.board.url list_post.pk %}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
{% endif %}
<div class="text-truncate new-list-title">{{ list_post.title }}</div>
<span class="ms-2 badge bg-secondary rounded-pill">{{ list_post.reply_count }}</span>
</a>
{% endfor %}
</ul>
</div>
prev_post 와 next_post 는 이따 처리해봅시다.
우선 html 에 전달하는 qs 데이터를 View 에서 직접 처리해서 보내도록 합시다.
html 내용을 보면 qs 를 자르고, 같은 id 가 있으면 그 영역의 색을 바꾸는 것 같습니다.
그러면 View 에서는 이 작업을 할 수 있게, qs 를 넘기는 게 아닌 DTO 를 만들어서 id, title, board.name, board.url, reply_count 를 넘기도록 합시다.
새로운 DTO 를 정의했습니다.
class RecentPost(BaseModel):
id: int = Field(..., description='게시글 ID')
title: str = Field(default='', description='제목')
reply_count: int = Field(default=0, description='댓글 수')
class RecentBoardPostLayer(BaseModel):
board_url: str = Field(..., description='게시판 URL')
board_name: str = Field(..., description='게시판 이름')
posts: Optional[List[RecentPost]] = Field(
default_factory=list,
description='게시판의 최근 게시물 목록',
)
새로운 DTO 를 이용해서 context 를 재정의합니다.
qs 를 recent_board_post_layer 로 수정했습니다.
def post_detail(request, board_url, pk):
active_filtered_posts = get_active_filtered_posts(board_urls=[board_url])
prev_post = active_filtered_posts.filter(id__lt=pk).first()
next_post = active_filtered_posts.filter(id__gt=pk).order_by('id').first()
post = get_object_or_404(active_filtered_posts, pk=pk)
post_summary = get_latest_post_summary_by_post_id(post.id)
context = {
'is_liked': bool(get_liked_post_ids_by_author_id(request.user.id, [post.id])),
'recent_board_post_layer': RecentBoardPostLayer(
board_url=board_url,
board_name=post.board.name,
posts=[
RecentPost(
id=recent_post.id,
title=recent_post.title,
reply_count=recent_post.reply_count + recent_post.rereply_count,
)
for recent_post in active_filtered_posts.order_by('-id')[:5]
],
).model_dump(),
'post': post,
'post_summary': post_summary,
'prev_post': prev_post,
'next_post': next_post,
}
return render(request, 'board/post.html', context)
정의한 context 기준으로 html 을 수정합니다.
<!-- 다른 게시판에 있는 게시글 목록 최신 -->
<div class="card mb-3">
<ul class="list-group">
<li class="list-group-item fw-bolder">{{ recent_board_post_layer.board_name }} 최신 목록</li>
{% for recent_post in recent_board_post_layer.posts %}
{% if post.id == recent_post.id %}
<a href="{% url 'board:post' recent_board_post_layer.board_url recent_post.id %}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center active">
{% else %}
<a href="{% url 'board:post' recent_board_post_layer.board_url recent_post.id %}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
{% endif %}
<div class="text-truncate new-list-title">{{ recent_post.title }}</div>
<span class="ms-2 badge bg-secondary rounded-pill">{{ recent_post.reply_count }}</span>
</a>
{% endfor %}
</ul>
</div>
잘 해결 됐습니다~!
이제는 post 입니다.
post.board.url post.board.name post.board.info post.id post.title post.body|striptags|slice:":100" post.post_img post.post_img.url post.author.nickname post.created_at|date:"Y-m-d" post.body|safe post.like_count post.reply_count post.urlimportants.all post.tag_set.all post.replys.all --> reply.rereplys.all
이렇게 사용하고 있습니다.
아찔한 부분은 post.replys.all 랑 reply.rereplys.all 부분일 것 같습니다.
차근 차근 해결해 봅시다.
우선 post 를 이용해서 가져오고 있는 외부 테이블을 직접 View 에서 테이블을 조회하도록 로직을 수정할 것입니다.
post.urlimportants.all
post.tag_set.all
post.replys.all
reply.rereplys.all
def get_url_importants(post_id: int) -> List[UrlImportant]:
return list(
UrlImportant.objects.filter(
post_id=post_id,
)
)
Service Layer 도 만들고~
class ImportantUrl(BaseModel):
url: str = Field(..., description='URL')
DTO 도 만들고~
context = {
'is_liked': bool(get_liked_post_ids_by_author_id(request.user.id, [post.id])),
'recent_board_post_layer': RecentBoardPostLayer(
board_url=board_url,
board_name=post.board.name,
posts=[
RecentPost(
id=recent_post.id,
title=recent_post.title,
reply_count=recent_post.reply_count,
)
for recent_post in active_filtered_posts.order_by('-id')[:5]
],
).model_dump(),
'post': post,
'post_summary': post_summary,
'prev_post': prev_post,
'next_post': next_post,
'important_urls': [
ImportantUrl(url=url_important.url)
for url_important in get_url_importants(post.id)
],
}
context 에 important_urls 로 정의해 줍니다.
<!-- 강조 url -->
<div class="my-3">
{% if important_urls %}
<div class="fw-bolder">URL</div>
{% for important_url in important_urls %}
<div>
<a href="{{ important_url.url }}">{{ important_url.url }}</a>
</div>
{% endfor %}
{% endif %}
</div>
HTML 도 수정합니다.
아래에 사용중인 Tag 를 가져오는 내용이 있습니다. 이것도 post.urlimportants.all 개선했던 것처럼 유사하게 View 에서 데이터를 전달하는 방식으로 만듭시다.
post_id 로 Tag 를 조회할 수 있는 Service Layer 를 만들어 줍니다.
def get_tags_by_post_id(post_id: int) -> List[Tag]:
qs = Post.tag_set.through.objects.select_related(
'tag',
).filter(
post_id=post_id,
)
return list(
q.tag
for q in qs
)
DTO 도 만들어 줍니다.
class DetailPostTag(BaseModel):
name: str = Field(..., description='태그 이름')
context 는 이렇게 정의해 줍니다.
context = {
...
'tags': [
DetailPostTag(name=tag.tag_name)
for tag in get_tags_by_post_id(post.id)
],
}
html 에도 잘 재정의 합니다.
<div class="mb-2">
<!-- 태그 -->
{% for tag in tags %}
<a href="{% url 'board:get_tagged_posts' tag.name %}" class="text-decoration-none">#{{ tag.name }}</a>
{% endfor %}
</div>
이거는 같이 처리해야합니다. 이유는 기존 로직에서는 Django Model 객체 기반으로 Reply 에서 Rereply 모델을 접근하는 방식으로 했기 때문이죠.
아래 코드를 보면 알 수 있습니다.
<!-- 댓글 + 대댓글 -->
<div class="mb-2">
<!-- 댓글 -->
{% for reply in post.replys.all %}
<div class="card mb-3 py-2">
<div id="reply_{{ reply.id }}" class="row g-0">
<div class="text-end pe-2">
{% if user.is_authenticated %}
<i class="reply_call bi bi-reply-fill"></i>
{% if request.user == reply.author or request.user.is_superuser %}
<i class="reply_del bi bi-trash-fill"></i>
{% endif %}
{% endif %}
</div>
<div class="d-flex mx-2">
<div class="align-self-center text-center"
style="display: flex;flex-direction: column;justify-content: center;align-items: center;">
{% if reply.author.user_img %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{{ reply.author.user_img.url }}" alt="{{ reply.author.nickname }}">
{% else %}
{% if reply.author.provider %}
{% if reply.author.provider.name == 'naver' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'naver_icon.png' %}" alt="{{ reply.author.nickname }}">
{% elif reply.author.provider.name == 'google' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'google_icon.png' %}" alt="{{ reply.author.nickname }}">
{% elif reply.author.provider.name == 'kakao' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'kakao_icon.png' %}" alt="{{ reply.author.nickname }}">
{% elif reply.author.provider.name == 'github' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'github_icon.png' %}" alt="{{ reply.author.nickname }}">
{% endif %}
{% else %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'logo.ico' %}" alt="{{ reply.author.nickname }}">
{% endif %}
{% endif %}
<div>{{ reply.author.nickname }}</div>
</div>
<div style="overflow-wrap: anywhere; margin-left: 5px;">
<div class="p-1">
<div class="card-text mb-1 ps-1 pe-3">{{ reply.body }}</div>
<div class="card-text"><small
class="text-muted">{{ reply.created_at|date:'Y-m-d' }}</small></div>
</div>
</div>
</div>
</div>
{% for rereply in reply.rereplys.all %}
<div id="rereply_{{ rereply.id }}" class="row g-0 mt-3">
<div class="text-end pe-2">
{% if user.is_authenticated %}
{% if request.user == rereply.author or request.user.is_superuser %}
<i id="rereply_{{ rereply.id }}" class="rereply_del bi bi-trash-fill"></i>
{% endif %}
{% endif %}
</div>
<div class="d-flex mx-2">
<div class="align-self-center text-center me-2"><i class="bi bi-arrow-return-right"></i>
</div>
<div class="align-self-center text-center">
{% if rereply.author.user_img %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{{ rereply.author.user_img.url }}" alt="{{ rereply.author.nickname }}">
{% else %}
{% if rereply.author.provider %}
{% if rereply.author.provider.name == 'naver' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'naver_icon.png' %}" alt="{{ rereply.author.nickname }}">
{% elif rereply.author.provider.name == 'google' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'google_icon.png' %}" alt="{{ rereply.author.nickname }}">
{% elif rereply.author.provider.name == 'kakao' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'kakao_icon.png' %}" alt="{{ rereply.author.nickname }}">
{% elif rereply.author.provider.name == 'github' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'github_icon.png' %}" alt="{{ rereply.author.nickname }}">
{% endif %}
{% else %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'logo.ico' %}" alt="{{ rereply.author.nickname }}">
{% endif %}
{% endif %}
<div>{{ rereply.author.nickname }}</div>
</div>
<div style="overflow-wrap: anywhere; margin-left: 5px;">
<div class="p-1">
<div class="card-text mb-1 ps-1 pe-3">{{ rereply.body }}</div>
<div class="card-text"><small
class="text-muted">{{ rereply.created_at|date:'Y-m-d' }}</small></div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
어우... 보기만해도 뭐가 뭔지 알기 어렵습니다.
이제 이를 명시적으로 어떤 객체의 무엇을 사용하는지를 HTML 에서 관리하는 게 아닌 View 에서 관리하도록 리팩토링해봅시다.
우선 필요한 데이터부터 정리해 봅시다.
post.replys.all
reply.id reply.author reply.author.user_img reply.author.user_img.url reply.author.nickname reply.author.provider reply.author.provider.name reply.body reply.created_at|date:'Y-m-d'
reply.rereplys.all
rereply.id rereply.author rereply.author.user_img rereply.author.user_img.url rereply.author.nickname rereply.author.provider rereply.author.provider.name rereply.body rereply.created_at|date:'Y-m-d'
구조는 Reply 내부에 Rereply 가 있는 방향으로 기존과 똑같이 정의하려고 합니다.
우선 필요한 데이터를 가져오고 위한 Service Layer 를 생성합시다.
def get_replys_by_post_id(post_id: int) -> QuerySet[Reply]:
return Reply.objects.filter(
post_id=post_id,
)
def get_rereplys_by_post_id(post_id: int) -> QuerySet[Rereply]:
return Rereply.objects.filter(
post_id=post_id,
)
def get_value_rereplys_key_rereply_reply_ids_by_post_id(post_id: int) -> DefaultDict[int, List[Rereply]]:
rereply_by_reply_ids = defaultdict(list)
for rereply in get_rereplys_by_post_id(post_id).select_related('author__provider'):
rereply_by_reply_ids[rereply.reply_id].append(rereply)
return rereply_by_reply_ids
DTO 생성합니다.
class DetailPostRereply(BaseModel):
id: int = Field(..., description='대댓글 ID')
body: str = Field(..., description='대댓글 본문')
author_id: int = Field(..., description='작성자 ID')
author_image_url: Optional[str] = Field(..., description='작성자 프로필 사진')
author_nickname: Optional[str] = Field(..., description='작성자 닉네임')
author_provider_name: Optional[str] = Field(..., description='작성자 소셜 로그인 제공자 이름')
created_at: str = Field(..., description='작성일')
class DetailPostReply(BaseModel):
id: int = Field(..., description='댓글 ID')
body: str = Field(..., description='댓글 본문')
author_id: int = Field(..., description='작성자 ID')
author_image_url: Optional[str] = Field(..., description='작성자 프로필 사진')
author_nickname: Optional[str] = Field(..., description='작성자 닉네임')
author_provider_name: Optional[str] = Field(..., description='작성자 소셜 로그인 제공자 이름')
created_at: str = Field(..., description='작성일')
rereplies: Optional[List[DetailPostRereply]] = Field(
default_factory=list,
description='대댓글 목록',
)
context 를 재정의 합니다.
replies = get_replys_by_post_id(post.id).select_related('author__provider')
rereplies_by_reply_ids = get_value_rereplies_key_rereply_reply_ids_by_post_id(post.id)
context = {
...
'replies': [
DetailPostReply(
id=reply.id,
body=reply.body,
author_id=reply.author_id,
author_image_url=(
reply.author.user_img.url
if reply.author.user_img else None
),
author_nickname=reply.author.nickname,
author_provider_name=reply.author.provider and reply.author.provider.provider_name,
created_at=reply.created_at.strftime('%Y-%m-%d'),
rereplies=[
DetailPostRereply(
id=rereply.id,
body=rereply.body,
author_id=rereply.author_id,
author_image_url=(
rereply.author.user_img.url
if rereply.author.user_img else None
),
author_nickname=rereply.author.nickname,
author_provider_name=rereply.author.provider and rereply.author.provider.provider_name,
created_at=rereply.created_at.strftime('%Y-%m-%d'),
)
for rereply in rereplies_by_reply_ids[reply.id]
]
) for reply in replies
],
}
이제 html 내용을 재정의 합니다. 중간에 있는 html if 문도 전부 정리할 수 있지만 요고는 나중에 리팩토링 하기 위해서 남겨두도록 하겠습니다.
<!-- 댓글 + 대댓글 -->
<div class="mb-2">
<!-- 댓글 -->
{% for reply in replies %}
<div class="card mb-3 py-2">
<div id="reply_{{ reply.id }}" class="row g-0">
<div class="text-end pe-2">
{% if user.is_authenticated %}
<i class="reply_call bi bi-reply-fill"></i>
{% if request.user.id == reply.author_id or request.user.is_superuser %}
<i class="reply_del bi bi-trash-fill"></i>
{% endif %}
{% endif %}
</div>
<div class="d-flex mx-2">
<div class="align-self-center text-center"
style="display: flex;flex-direction: column;justify-content: center;align-items: center;">
{% if reply.author_image_url %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{{ reply.author_image_url }}" alt="{{ reply.author_nickname }}">
{% else %}
{% if reply.author_provider_name == 'naver' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'naver_icon.png' %}" alt="{{ reply.author_nickname }}">
{% elif reply.author_provider_name == 'google' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'google_icon.png' %}" alt="{{ reply.author_nickname }}">
{% elif reply.author_provider_name == 'kakao' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'kakao_icon.png' %}" alt="{{ reply.author_nickname }}">
{% elif reply.author_provider_name == 'github' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'github_icon.png' %}" alt="{{ reply.author_nickname }}">
{% else %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'logo.ico' %}" alt="{{ reply.author_nickname }}">
{% endif %}
{% endif %}
<div>{{ reply.author_nickname }}</div>
</div>
<div style="overflow-wrap: anywhere; margin-left: 5px;">
<div class="p-1">
<div class="card-text mb-1 ps-1 pe-3">{{ reply.body }}</div>
<div class="card-text"><small
class="text-muted">{{ reply.created_at }}</small></div>
</div>
</div>
</div>
</div>
{% for rereply in reply.rereplies %}
<div id="rereply_{{ rereply.id }}" class="row g-0 mt-3">
<div class="text-end pe-2">
{% if user.is_authenticated %}
{% if request.user.id == rereply.author_id or request.user.is_superuser %}
<i id="rereply_{{ rereply.id }}" class="rereply_del bi bi-trash-fill"></i>
{% endif %}
{% endif %}
</div>
<div class="d-flex mx-2">
<div class="align-self-center text-center me-2"><i class="bi bi-arrow-return-right"></i>
</div>
<div class="align-self-center text-center">
{% if rereply.author_image_url %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{{ rereply.author_image_url }}" alt="{{ rereply.author_nickname }}">
{% else %}
{% if rereply.author_provider_name == 'naver' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'naver_icon.png' %}" alt="{{ rereply.author_nickname }}">
{% elif rereply.author_provider_name == 'google' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'google_icon.png' %}" alt="{{ rereply.author_nickname }}">
{% elif rereply.author_provider_name == 'kakao' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'kakao_icon.png' %}" alt="{{ rereply.author_nickname }}">
{% elif rereply.author_provider_name == 'github' %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'github_icon.png' %}" alt="{{ rereply.author_nickname }}">
{% else %}
<img class="img-thumbnail align-middle" style="max-width:70px;max-height: 70px;"
src="{% static 'logo.ico' %}" alt="{{ rereply.author_nickname }}">
{% endif %}
{% endif %}
<div>{{ rereply.author_nickname }}</div>
</div>
<div style="overflow-wrap: anywhere; margin-left: 5px;">
<div class="p-1">
<div class="card-text mb-1 ps-1 pe-3">{{ rereply.body }}</div>
<div class="card-text"><small
class="text-muted">{{ rereply.created_at }}</small></div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
이제는 post 부분 남았습니다. View, DTO, HTML 을 수정합시다.
Dto
class DetailPost(BaseModel):
id: int = Field(..., description='게시글 ID')
board_url: Optional[str] = Field(..., description='게시판 URL')
board_name: Optional[str] = Field(..., description='게시판 이름')
board_info: Optional[str] = Field(..., description='게시판 정보')
author_nickname: Optional[str] = Field(..., description='작성자 닉네임')
title: str = Field(..., description='제목')
simple_body: str = Field(..., description='본문의 앞부분 100자')
body: Any = Field(..., description='본문 전체')
main_image_url: Optional[str] = Field(..., description='대표 이미지 URL')
like_count: int = Field(..., description='좋아요 수')
reply_count: int = Field(..., description='댓글 수')
created_at: str = Field(..., description='작성일')
body 를 Any 로 정의한 이유는 html 에게 넘겨줄 때, SafeString 으로 넘겨주기 위함입니다.
View
context = {
...
'post': DetailPost(
id=post.id,
board_url=post.board.url,
board_name=post.board.name,
author_nickname=post.author.nickname,
title=post.title,
simple_body=post.short_body(),
body=post.body,
main_image_url=(
post.post_img.url if post.post_img else None
),
like_count=post.like_count,
reply_count=post.reply_count + post.rereply_count,
created_at=post.created_at.strftime('%Y-%m-%d'),
),
...
}
Service Layer 는 이미 존재해서 DTO랑 Html 만 정의하면 될것 같습니다.
class DetailPostSummary(BaseModel):
status: str = Field(..., description='상태')
body: Optional[str] = Field(..., description='본문')
context = {
...
'post_summary': DetailPostSummary(
status=post_summary.status,
body=post_summary.body,
) if post_summary else None,
...
}
prev_post 와 next_post 는 post 의 id 와 board_url 만 있으면 됩니다. 빠르게 정리해보죠!!
DTO를 만들고, view 에 정의하고 html 도 수정합니다.
class DetailPostNavigation(BaseModel):
post_id: int = Field(..., description='게시글 ID')
board_url: Optional[str] = Field(..., description='게시판 URL')
context = {
...
'prev_post_navigation': DetailPostNavigation(
post_id=prev_post.id,
board_url=prev_post.board.url,
) if prev_post else None,
'next_post_navigation': DetailPostNavigation(
post_id=next_post.id,
board_url=next_post.board.url,
) if next_post else None,
...
}
아래와 같이 View 를 재 정의 했습니다. 이제는 context 를 Dict 가 아닌 DTO 로 만들어서 제공하려고 합니다.
def post_detail(request, board_url: str, pk: int):
active_filtered_posts = get_active_filtered_posts(board_urls=[board_url])
prev_post = active_filtered_posts.filter(id__lt=pk).last()
next_post = active_filtered_posts.filter(id__gt=pk).order_by('id').first()
post = get_object_or_404(active_filtered_posts.select_related('author'), pk=pk)
post_summary = get_latest_post_summary_by_post_id(post.id)
replies = get_replys_by_post_id(post.id).select_related('author__provider')
rereplies_by_reply_ids = get_value_rereplies_key_rereply_reply_ids_by_post_id(post.id)
context = {
'is_liked': bool(get_liked_post_ids_by_author_id(request.user.id, [post.id])),
'recent_board_post_layer': RecentBoardPostLayer(
board_url=board_url,
board_name=post.board.name,
posts=[
RecentPost(
id=recent_post.id,
title=recent_post.title,
reply_count=recent_post.reply_count + recent_post.rereply_count,
)
for recent_post in active_filtered_posts.order_by('-id')[:5]
],
).model_dump(),
'post': DetailPost(
id=post.id,
board_url=post.board.url,
board_name=post.board.name,
board_info=post.board.info,
author_nickname=post.author.nickname,
title=post.title,
simple_body=post.short_body(),
body=mark_safe(post.body),
main_image_url=(
post.post_img.url if post.post_img else None
),
like_count=post.like_count,
reply_count=post.reply_count + post.rereply_count,
created_at=post.created_at.strftime('%Y-%m-%d'),
),
'post_summary': DetailPostSummary(
status=post_summary.status,
body=post_summary.body,
) if post_summary else None,
'prev_post_navigation': DetailPostNavigation(
post_id=prev_post.id,
board_url=prev_post.board.url,
) if prev_post else None,
'next_post_navigation': DetailPostNavigation(
post_id=next_post.id,
board_url=next_post.board.url,
) if next_post else None,
'important_urls': [
ImportantUrl(url=url_important.url)
for url_important in get_url_importants(post.id)
],
'tags': [
DetailPostTag(name=tag.tag_name)
for tag in get_tags_by_post_id(post.id)
],
'replies': [
DetailPostReply(
id=reply.id,
body=reply.body,
author_id=reply.author_id,
author_image_url=(
reply.author.user_img.url
if reply.author.user_img else None
),
author_nickname=reply.author.nickname,
author_provider_name=reply.author.provider and reply.author.provider.provider_name,
created_at=reply.created_at.strftime('%Y-%m-%d'),
rereplies=[
DetailPostRereply(
id=rereply.id,
body=rereply.body,
author_id=rereply.author_id,
author_image_url=(
rereply.author.user_img.url
if rereply.author.user_img else None
),
author_nickname=rereply.author.nickname,
author_provider_name=rereply.author.provider and rereply.author.provider.provider_name,
created_at=rereply.created_at.strftime('%Y-%m-%d'),
)
for rereply in rereplies_by_reply_ids[reply.id]
]
) for reply in replies
],
}
return render(request, 'board/post.html', context)
class PostDetailResponse(BaseModel):
is_liked: bool = Field(..., description='좋아요 여부')
recent_board_post_layer: RecentBoardPostLayer = Field(..., description='최근 게시물 목록')
post: DetailPost = Field(..., description='게시글 정보')
post_summary: Optional[DetailPostSummary] = Field(..., description='게시글 요약 정보')
prev_post_navigation: Optional[DetailPostNavigation] = Field(..., description='이전 게시글 정보')
next_post_navigation: Optional[DetailPostNavigation] = Field(..., description='다음 게시글 정보')
important_urls: List[ImportantUrl] = Field(default_factory=list, description='중요 URL 목록')
tags: List[DetailPostTag] = Field(default_factory=list, description='태그 목록 정보')
replies: List[DetailPostReply] = Field(default_factory=list, description='댓글 목록')
View
많이 길지만 나중에 classmethod 로 따로 특정 객체를 이용해서 정의하면 길이를 짧게 만들 수 있습니다.
저는 어느정도 직관적으로 우선 보여주는 걸 좋아해서 여기까지 리팩토링 해보겠습니다.
def post_detail(request, board_url: str, pk: int):
active_filtered_posts = get_active_filtered_posts(board_urls=[board_url])
prev_post = active_filtered_posts.filter(id__lt=pk).last()
next_post = active_filtered_posts.filter(id__gt=pk).order_by('id').first()
post = get_object_or_404(active_filtered_posts.select_related('author'), pk=pk)
post_summary = get_latest_post_summary_by_post_id(post.id)
replies = get_replys_by_post_id(post.id).select_related('author__provider')
rereplies_by_reply_ids = get_value_rereplies_key_rereply_reply_ids_by_post_id(post.id)
return render(
request,
'board/post.html',
PostDetailResponse(
is_liked=bool(get_liked_post_ids_by_author_id(request.user.id, [post.id])),
recent_board_post_layer=RecentBoardPostLayer(
board_url=board_url,
board_name=post.board.name,
posts=[
RecentPost(
id=recent_post.id,
title=recent_post.title,
reply_count=recent_post.reply_count + recent_post.rereply_count,
)
for recent_post in active_filtered_posts.order_by('-id')[:5]
],
),
post=DetailPost(
id=post.id,
board_url=post.board.url,
board_name=post.board.name,
board_info=post.board.info,
author_nickname=post.author.nickname,
title=post.title,
simple_body=post.short_body(),
body=mark_safe(post.body),
main_image_url=(
post.post_img.url if post.post_img else None
),
like_count=post.like_count,
reply_count=post.reply_count + post.rereply_count,
created_at=post.created_at.strftime('%Y-%m-%d'),
),
post_summary=DetailPostSummary(
status=post_summary.status,
body=post_summary.body,
) if post_summary else None,
prev_post_navigation=DetailPostNavigation(
post_id=prev_post.id,
board_url=prev_post.board.url,
) if prev_post else None,
next_post_navigation=DetailPostNavigation(
post_id=next_post.id,
board_url=next_post.board.url,
) if next_post else None,
important_urls=[
ImportantUrl(url=url_important.url)
for url_important in get_url_importants(post.id)
],
tags=[
DetailPostTag(name=tag.tag_name)
for tag in get_tags_by_post_id(post.id)
],
replies=[
DetailPostReply(
id=reply.id,
body=reply.body,
author_id=reply.author_id,
author_image_url=(
reply.author.user_img.url
if reply.author.user_img else None
),
author_nickname=reply.author.nickname,
author_provider_name=reply.author.provider and reply.author.provider.provider_name,
created_at=reply.created_at.strftime('%Y-%m-%d'),
rereplies=[
DetailPostRereply(
id=rereply.id,
body=rereply.body,
author_id=rereply.author_id,
author_image_url=(
rereply.author.user_img.url
if rereply.author.user_img else None
),
author_nickname=rereply.author.nickname,
author_provider_name=rereply.author.provider and rereply.author.provider.provider_name,
created_at=rereply.created_at.strftime('%Y-%m-%d'),
)
for rereply in rereplies_by_reply_ids[reply.id]
]
) for reply in replies
],
).model_dump(),
)