diff --git a/posts/apps.py b/posts/apps.py index 81782a2..cd95585 100644 --- a/posts/apps.py +++ b/posts/apps.py @@ -4,3 +4,6 @@ class PostsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "posts" + + def ready(self): + import posts.signals # noqa diff --git a/posts/serializers.py b/posts/serializers.py index 3a36501..0dfd789 100644 --- a/posts/serializers.py +++ b/posts/serializers.py @@ -74,3 +74,23 @@ class PostQuerySerializer(serializers.Serializer): help_text="created_at, updated_at, view_count, like_count, share_count를 기준으로 오름차순/내림차순으로 정렬하여 조회합니다. (default: created_at)", ) hashtag = serializers.CharField(required=False, help_text="조회할 해시태그입니다. (default: 본인계정)") + + +class PostDetailSerializer(serializers.ModelSerializer): + hashtag = HashTagSerializer(many=True, read_only=True) + + class Meta: + model = Post + fields = [ + "content_id", + "post_type", + "title", + "content", + "view_count", + "like_count", + "share_count", + "created_at", + "updated_at", + "hashtag", + "user", + ] diff --git a/posts/signals.py b/posts/signals.py new file mode 100644 index 0000000..c1115f0 --- /dev/null +++ b/posts/signals.py @@ -0,0 +1,10 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver + +from posts.models import Post + + +@receiver(pre_save, sender=Post) +def increment_view_count(sender, instance, **kwargs): + # 게시글을 조회할 때 조회수 증가 + instance.view_count += 1 diff --git a/posts/tests/views/test_posts_detail_view.py b/posts/tests/views/test_posts_detail_view.py new file mode 100644 index 0000000..e39302b --- /dev/null +++ b/posts/tests/views/test_posts_detail_view.py @@ -0,0 +1,84 @@ +import uuid + +from django.contrib.auth import get_user_model +from django.db import transaction +from django.urls import reverse +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APITestCase + +from posts.models import HashTag, Post + +User = get_user_model() + + +class PostDetailViewTest(APITestCase): + @classmethod + def setUpTestData(cls): + # 테스트 사용자 생성 + cls.user = User.objects.create_user(username="testuser1", password="testpass1", email="test1@email.com") + + # 테스트 해시태그 생성 + cls.hashtag = HashTag.objects.create(name="hashtag1") + + # 테스트 게시물 생성 및 content_id 저장 + cls.content_ids = [] + + # 테스트 게시물 생성 + for i in range(1, 6): + post = Post.objects.create( + content_id=uuid.uuid4(), + user=cls.user, + post_type="facebook", + title=f"test title {i}", + content=f"test content {i}", + view_count=10 * i, + like_count=5 * i, + share_count=2 * i, + created_at=timezone.now(), + updated_at=timezone.now(), + ) + post.hashtag.set([cls.hashtag]) + cls.content_ids.append(str(post.content_id)) + + def setUp(self): + pass + + def test_get_existing_post_content_id(self): + response = self.client.get( + path=reverse("post-detail", kwargs={"content_id": self.content_ids[0]}), + data={"type": "all", "hashtag": self.hashtag.id, "ordering": "created_at", "search": ""}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_get_not_existing_post_content_id(self): + non_existing_content_id = "00000000-0000-0000-0000-000000000001" + response = self.client.get( + path=reverse("post-detail", kwargs={"content_id": non_existing_content_id}), + data={"type": "all", "hashtag": self.hashtag.id, "ordering": "created_at", "search": ""}, + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_increment_view_count(self): + content_id = self.content_ids[0] + + # 현재 조회수 가져오기 + # 초기값 - 1 (조회와 동시에 count가 됨) + post = Post.objects.get(content_id=content_id) + initial_view_count = post.view_count - 1 + + # 게시물 조회 + with transaction.atomic(): + response = self.client.get( + path=reverse("post-detail", kwargs={"content_id": content_id}), + data={"type": "all", "hashtag": self.hashtag.id, "ordering": "created_at", "search": ""}, + ) + + # 조회수 업데이트 확인 + post.refresh_from_db() + updated_view_count = post.view_count + + print("initial_view_count", initial_view_count) + + self.assertEqual(updated_view_count, initial_view_count + 1) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/posts/urls.py b/posts/urls.py index 714b6ee..302e8ab 100644 --- a/posts/urls.py +++ b/posts/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from posts.views import PostListView, StatisticsListView +from posts.views import PostDetailView, PostListView, StatisticsListView urlpatterns = [ path("statistics/", StatisticsListView.as_view(), name="statistics"), path("", PostListView.as_view(), name="list"), + path("/", PostDetailView.as_view(), name="post-detail"), ] diff --git a/posts/views.py b/posts/views.py index a9e3a6b..29a446c 100644 --- a/posts/views.py +++ b/posts/views.py @@ -3,6 +3,7 @@ from django.db.models import Count, Q, Sum from django.db.models.functions import TruncDay, TruncHour from django.db.models.query import QuerySet +from django.shortcuts import get_object_or_404 from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.pagination import LimitOffsetPagination @@ -18,6 +19,7 @@ from posts.models import Post from posts.paginations import PaginationHandlerMixin from posts.serializers import ( + PostDetailSerializer, PostListSerializer, PostQuerySerializer, StatisticsListSerializer, @@ -210,3 +212,20 @@ def get_ordering(self, posts: QuerySet[Post], ordering: str) -> QuerySet[Post]: return posts.order_by(ordering) else: raise InvalidParameterException(f"ordering 값 {ordering}를 잘못 선택하셨습니다.") + + +class PostDetailView(APIView): + # @TODO: IsAuthenticated로 변경 @simseulnyang + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_summary="content_id에 해당하는 게시글 상세 조회", + responses={ + status.HTTP_200_OK: PostDetailSerializer, + }, + ) + def get(self, request: Request, content_id: str) -> Response: + post = get_object_or_404(Post, content_id=content_id) + serializer = PostDetailSerializer(post) + + return Response(serializer.data, status=status.HTTP_200_OK)