백엔드 개발
태그
class Post(TimeStampedModel):
...
tags = models.ManyToManyField(
"tags.Tag", through="tags.PostTag", related_name="posts"
)
through="tags.PostTag" 옵션
- Django가 자동으로 숨겨진 중간 테이블을 만들도록 내버려 두지 않고
- tags 앱에 정의된 PostTag라는 모델을 연결 테이블로 직접 명시해서 사용
기존 기능에 태그 추가
class PostListSerializer(serializers.ModelSerializer):
...
tags = serializers.SlugRelatedField(
many=True, read_only=True, slug_field="name"
)
class Meta:
model = Post
fields = [
...
"tags",
]
def get_global_posts() -> QuerySet[Post]:
"""
모든 사용자의 공개된 포스트 목록을 가져옵니다. (전체 피드용)
"""
return (
Post.objects.filter(
is_temp=False,
visibility=Post.Visibility.PUBLIC,
deleted_at__isnull=True,
)
.select_related("user")
.prefetch_related("tags")
.annotate(likes_count=Count("likes", distinct=True))
.order_by("-created_at")
)
def get_tags_with_post_counts() -> QuerySet[Tag]:
"""태그 목록과 각 태그별 유효한 게시글 수를 반환하는 서비스 로직입니다."""
valid_post_count = Count(
"posts",
filter=Q(
posts__is_temp=False,
posts__deleted_at__isnull=True,
posts__visibility=Post.Visibility.PUBLIC,
),
distinct=True,
)
return (
Tag.objects.annotate(
post_count=valid_post_count
)
.filter(post_count__gt=0)
.order_by(
"-post_count", "name"
)
)
class TagStatSerializer(serializers.ModelSerializer):
"""홈 화면 태그 클라우드용 시리얼라이저"""
post_count = serializers.IntegerField(read_only=True)
class Meta:
model = Tag
fields = ["id", "name", "post_count"]
class TagListAPIView(APIView):
"""홈 화면 등에 쓰일 전체 태그 목록 및 통계를 제공합니다."""
permission_classes = [AllowAny]
@extend_schema(tags=["태그"], summary="전체 태그 및 게시글 갯수 통계 조회")
def get(self, request):
tags = get_tags_with_post_counts()
serializer = TagStatSerializer(tags, many=True)
return Response(serializer.data)


댓글
작성
def create_comment(*, post_id: int, user: User, validated_data: dict) -> Comment:
"""댓글을 생성하는 서비스 로직입니다."""
post = Post.objects.filter(id=post_id, deleted_at__isnull=True).first()
if not post:
raise BaseCustomException(ErrorMessage.POST_NOT_FOUND)
comment = Comment.objects.create(
post=post, user=user, content=validated_data["content"]
)
return comment
class CommentCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["content"]
class CommentAPIView(APIView):
"""게시글의 댓글 작성을 담당하는 View입니다."""
permission_classes = [IsAuthenticated]
@extend_schema(tags=["댓글"], summary="댓글 작성", request=CommentCreateSerializer)
def post(self, request: Request, post_id: int):
serializer = CommentCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = cast(User, request.user)
create_comment(
post_id=post_id, user=user, validated_data=serializer.validated_data
)
return Response(
{"message": "댓글이 성공적으로 작성되었습니다."},
status=status.HTTP_201_CREATED,
)
조회
def get_post_comments(*, post_id: int) -> QuerySet[Comment]:
"""특정 게시글의 댓글 목록을 조회하는 서비스 로직입니다."""
post_exists = Post.objects.filter(id=post_id, deleted_at__isnull=True).exists()
if not post_exists:
raise BaseCustomException(ErrorMessage.POST_NOT_FOUND)
comments = (
Comment.objects.filter(post_id=post_id)
.select_related("user")
.order_by("created_at")
)
return comments
class CommentListSerializer(serializers.ModelSerializer):
"""댓글 목록 조회를 위한 시리얼라이저입니다."""
author_nickname = serializers.CharField(source="user.nickname", read_only=True)
class Meta:
model = Comment
fields = [
"id",
"author_nickname",
"content",
"created_at",
]
@extend_schema(tags=["댓글"], summary="게시글 댓글 목록 조회")
def get(self, request: Request, post_id: int):
"""GET 요청 시 특정 게시글의 댓글 목록을 페이지네이션하여 반환합니다."""
comments = get_post_comments(post_id=post_id)
paginator = self.pagination_class()
page = paginator.paginate_queryset(comments, request, view=self)
if page is not None:
serializer = CommentListSerializer(page, many=True)
return paginator.get_paginated_response(serializer.data)
return Response(CommentListSerializer(comments, many=True).data)
시리즈