From f8d267aae42db066e8634660eb9dfd8509232d3a Mon Sep 17 00:00:00 2001 From: jungmir Date: Fri, 27 Jun 2025 00:42:32 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Feat:=20=ED=99=9C=EB=8F=99=20=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0008_activityhistory.py | 43 +++++++++++++++++++ src/activity_gallery/models.py | 20 +++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/activity_gallery/migrations/0008_activityhistory.py diff --git a/src/activity_gallery/migrations/0008_activityhistory.py b/src/activity_gallery/migrations/0008_activityhistory.py new file mode 100644 index 0000000..f17dd2b --- /dev/null +++ b/src/activity_gallery/migrations/0008_activityhistory.py @@ -0,0 +1,43 @@ +# Generated by Django 5.1.5 on 2025-06-26 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("activity_gallery", "0007_alter_actionphoto_image_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ActivityHistory", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("is_deleted", models.BooleanField(default=False, verbose_name="숨김 여부")), + ("title", models.CharField(max_length=255, verbose_name="활동명")), + ("content", models.TextField(verbose_name="내용")), + ( + "thumbnail", + models.ImageField( + blank=True, + null=True, + upload_to="activity/history/thumbnail/", + verbose_name="활동 썸네일 이미지", + ), + ), + ("activity_date", models.DateField(verbose_name="활동 날짜")), + ("created_at", models.DateTimeField(auto_now_add=True, verbose_name="생성일")), + ("updated_at", models.DateTimeField(auto_now=True, verbose_name="수정일")), + ], + options={ + "verbose_name": "활동 히스토리", + "verbose_name_plural": "활동 히스토리", + "ordering": ("activity_date",), + }, + ), + ] diff --git a/src/activity_gallery/models.py b/src/activity_gallery/models.py index 50b3fc3..ee6feca 100644 --- a/src/activity_gallery/models.py +++ b/src/activity_gallery/models.py @@ -1,5 +1,6 @@ from django.db import models +from core.mixins.models import SoftDeleteModel from public.mixin.img_models import MultiImageFieldMixin from public.tag_models import ActivityTag, Tag @@ -39,3 +40,22 @@ class ActionPhoto(MultiImageFieldMixin): def __str__(self): return f"Photo for {self.activity_action.title if self.activity_action else 'No Activity'}" + + +class ActivityHistory(SoftDeleteModel): + title = models.CharField(max_length=255, verbose_name="활동명") + content = models.TextField(verbose_name="내용") + thumbnail = models.ImageField( + "활동 썸네일 이미지", upload_to="activity/history/thumbnail/", null=True, blank=True + ) + activity_date = models.DateField(verbose_name="활동 날짜") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="생성일") + updated_at = models.DateTimeField(auto_now=True, verbose_name="수정일") + + class Meta: + verbose_name = "활동 히스토리" + verbose_name_plural = "활동 히스토리" + ordering = ("activity_date",) + + def __str__(self): + return f"History for {self.title} at {self.activity_date}" From b267fd0ae38367133a9f4e3e58f7c6b31505d0dc Mon Sep 17 00:00:00 2001 From: jungmir Date: Fri, 27 Jun 2025 00:42:43 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Test:=20=ED=99=9C=EB=8F=99=20=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EB=AA=A8=EB=8D=B8=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/activity_gallery/tests.py | 1 - .../tests/test_activity_history_model.py | 174 ++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) delete mode 100644 src/activity_gallery/tests.py create mode 100644 src/activity_gallery/tests/test_activity_history_model.py diff --git a/src/activity_gallery/tests.py b/src/activity_gallery/tests.py deleted file mode 100644 index a39b155..0000000 --- a/src/activity_gallery/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/src/activity_gallery/tests/test_activity_history_model.py b/src/activity_gallery/tests/test_activity_history_model.py new file mode 100644 index 0000000..ebad14a --- /dev/null +++ b/src/activity_gallery/tests/test_activity_history_model.py @@ -0,0 +1,174 @@ +import pytest +from django.db.utils import DatabaseError + +from activity_gallery.models import ActivityHistory + + +@pytest.mark.django_db +@pytest.mark.feature +@pytest.mark.parametrize( + "title, content, activity_date", + [ + ("Activity 1", "Content 1", "2024-01-01"), + ("Activity 2", "Content 2", "2024-06-01"), + ("Activity 3", "Content 3", "2023-12-31"), + ], +) +def test_create_activity_history_success_given_valid_data(title, content, activity_date): + # Given + data = { + "title": title, + "content": content, + "activity_date": activity_date, + } + # When + history = ActivityHistory.objects.create(**data) + # Then + assert history.title == title + assert history.content == content + assert str(history.activity_date) == activity_date + + +@pytest.mark.django_db +@pytest.mark.feature +@pytest.mark.parametrize( + "title, content, activity_date", + [ + (None, "Content", "2024-01-01"), + ("Title", None, "2024-01-01"), + ("Title", "Content", None), + (None, None, None), + ], +) +def test_create_activity_history_fail_given_invalid_data(title, content, activity_date): + # Given + data = { + "title": title, + "content": content, + "activity_date": activity_date, + } + # Then + with pytest.raises((DatabaseError, ValueError, TypeError)): + # When + ActivityHistory.objects.create(**data) + + +@pytest.mark.django_db +@pytest.mark.feature +@pytest.mark.parametrize( + "title, content, activity_date", + [ + ("Activity 1", "Content 1", "2024-01-01"), + ("Activity 2", "Content 2", "2024-06-01"), + ], +) +def test_read_activity_history_given_exist_id(title, content, activity_date): + # Given + history = ActivityHistory.objects.create( + title=title, content=content, activity_date=activity_date + ) + # When + fetched = ActivityHistory.objects.get(id=history.id) + # Then + assert fetched.title == title + assert fetched.content == content + assert str(fetched.activity_date) == activity_date + + +@pytest.mark.django_db +@pytest.mark.feature +@pytest.mark.parametrize( + "history_id", + [-1, 0], +) +def test_read_activity_history_given_non_exist_id(history_id): + # Given + # When / Then + with pytest.raises(ActivityHistory.DoesNotExist): + ActivityHistory.objects.get(id=history_id) + + +@pytest.mark.django_db +@pytest.mark.feature +@pytest.mark.parametrize( + "title, content, activity_date, new_title, new_content, new_activity_date", + [ + ("Activity 1", "Content 1", "2024-01-01", "Updated 1", "Updated Content 1", "2024-02-01"), + ("Activity 2", "Content 2", "2024-06-01", "Updated 2", "Updated Content 2", "2024-07-01"), + ], +) +def test_update_activity_history_success_given_valid_data( + title, content, activity_date, new_title, new_content, new_activity_date +): + # Given + history = ActivityHistory.objects.create( + title=title, content=content, activity_date=activity_date + ) + # When + history.title = new_title + history.content = new_content + history.activity_date = new_activity_date + history.save() + updated = ActivityHistory.objects.get(id=history.id) + # Then + assert updated.title == new_title + assert updated.content == new_content + assert str(updated.activity_date) == new_activity_date + + +@pytest.mark.django_db +@pytest.mark.feature +@pytest.mark.parametrize( + "title, content, activity_date, new_title, new_content, new_activity_date", + [ + ("Activity 1", "Content 1", "2024-01-01", None, "Updated Content", "2024-02-01"), + ("Activity 2", "Content 2", "2024-06-01", "Updated", None, "2024-07-01"), + ("Activity 3", "Content 3", "2024-06-01", "Updated", "Updated Content", None), + ], +) +def test_update_activity_history_fail_given_invalid_data( + title, content, activity_date, new_title, new_content, new_activity_date +): + # Given + history = ActivityHistory.objects.create( + title=title, content=content, activity_date=activity_date + ) + # When + history.title = new_title + history.content = new_content + history.activity_date = new_activity_date + # Then + with pytest.raises((DatabaseError, ValueError, TypeError)): + history.save() + + +@pytest.mark.django_db +@pytest.mark.feature +def test_delete_activity_history_hard_delete(): + # Given + history = ActivityHistory.objects.create( + title="To Delete", content="Delete me", activity_date="2024-01-01" + ) + history_id = history.id + # When + # If using soft delete, use hard delete method; otherwise, keep as is + history.delete(force_delete=True) + # Then + with pytest.raises(ActivityHistory.DoesNotExist): + ActivityHistory.objects.get(id=history_id) + + +@pytest.mark.django_db +@pytest.mark.feature +def test_delete_activity_history_when_soft_delete_operation() -> None: + # Given + history = ActivityHistory.objects.create( + title="To Delete", content="Delete me", activity_date="2024-01-01" + ) + + # When + history.delete() + + # Then + deleted = ActivityHistory.objects.get(id=history.id) + assert deleted.is_deleted is True From e8b5faefb9a7c474caea8af07df16ff01a6495e4 Mon Sep 17 00:00:00 2001 From: jungmir Date: Fri, 27 Jun 2025 00:57:42 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Feat:=20=ED=99=9C=EB=8F=99=20=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=20viewset=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/activity_gallery/serializers.py | 13 ++++++- .../tests/test_activity_history_view.py | 38 +++++++++++++++++++ src/activity_gallery/views.py | 20 +++++++++- 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/activity_gallery/tests/test_activity_history_view.py diff --git a/src/activity_gallery/serializers.py b/src/activity_gallery/serializers.py index 6f331d6..1385171 100644 --- a/src/activity_gallery/serializers.py +++ b/src/activity_gallery/serializers.py @@ -3,7 +3,7 @@ from public.serializers import TagSerializer from public.tag_models import ActivityTag -from .models import ActionPhoto, ActivityAction +from .models import ActionPhoto, ActivityAction, ActivityHistory # 액션과 연결된 사진 정보를 직렬화하는 Serializer @@ -44,3 +44,14 @@ class Meta: "tags", "photos", ] # 상세 조회 시 모든 정보 포함 + + +class ActivityHistorySerializer(serializers.ModelSerializer): + class Meta: + model = ActivityHistory + fields = [ + "id", + "title", + "content", + "activity_date", + ] diff --git a/src/activity_gallery/tests/test_activity_history_view.py b/src/activity_gallery/tests/test_activity_history_view.py new file mode 100644 index 0000000..ac7ff50 --- /dev/null +++ b/src/activity_gallery/tests/test_activity_history_view.py @@ -0,0 +1,38 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + +from activity_gallery.models import ActivityHistory + + +class ActivityHistoryViewSetTest(APITestCase): + def setUp(self): + self.history1 = ActivityHistory.objects.create( + title="History 1", + content="Content 1", + activity_date="2024-01-01", + is_deleted=False, + ) + self.history2 = ActivityHistory.objects.create( + title="History 2", + content="Content 2", + activity_date="2024-02-01", + is_deleted=False, + ) + self.deleted_history = ActivityHistory.objects.create( + title="Deleted History", + content="Should not show up.", + activity_date="2024-03-01", + is_deleted=True, + ) + + def test_list_activity_history(self): + url = reverse("activity-history-list") + response = self.client.get(url, query_params={"page": 1}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertIn("data", response.data) + returned_ids = [item["id"] for item in response.data["data"]] + self.assertIn(self.history1.id, returned_ids) + self.assertIn(self.history2.id, returned_ids) + self.assertNotIn(self.deleted_history.id, returned_ids) diff --git a/src/activity_gallery/views.py b/src/activity_gallery/views.py index 32ed697..205eb4a 100644 --- a/src/activity_gallery/views.py +++ b/src/activity_gallery/views.py @@ -4,12 +4,13 @@ from core.responses.base import BaseResponse -from .models import ActivityAction +from .models import ActivityAction, ActivityHistory from .serializers import ( ActivityActionDetailSerializer, ActivityActionListSerializer, + ActivityHistorySerializer, ) -from .swagger import ActivityActionAPIDocs +from .swagger import ActivityActionAPIDocs, ActivityHistoryAPIDocs @extend_schema_view( @@ -42,3 +43,18 @@ def retrieve(self, request, *args, **kwargs) -> BaseResponse: instance = self.get_object() serializer = self.get_serializer(instance) return BaseResponse(serializer.data) + + +@extend_schema_view(list=ActivityHistoryAPIDocs.list()) +class ActivityHistoryViewSet(GenericViewSet, ListModelMixin): + """활동 히스토리 API 뷰""" + + queryset = ActivityHistory.objects.filter(is_deleted=False).all() + serializer_class = ActivityHistorySerializer + + def list(self, request, *args, **kwargs) -> BaseResponse: + """활동 히스토리 목록 조회 API""" + queryset = self.get_queryset() + page = self.paginate_queryset(queryset) + serializer = self.get_serializer(page or queryset, many=True) + return self.get_paginated_response(serializer.data) From 8469d8e0c3353ba9d46f7a9f4487e4abdebbaca4 Mon Sep 17 00:00:00 2001 From: jungmir Date: Fri, 27 Jun 2025 00:58:14 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Feat:=20=ED=99=9C=EB=8F=99=20=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=20API=EB=A5=BC=20router=EC=97=90=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/activity_gallery/swagger.py | 55 +++++++++++++++++++++++++++++++++ src/activity_gallery/urls.py | 3 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/activity_gallery/swagger.py b/src/activity_gallery/swagger.py index 65cfbcf..354421f 100644 --- a/src/activity_gallery/swagger.py +++ b/src/activity_gallery/swagger.py @@ -102,3 +102,58 @@ def retrieve(cls): description="특정 커뮤니티 활동 상세 조회", responses=responses, ) + + +class ActivityHistoryAPIDocs(SwaggerSchema): + """활동 히스토리 API 문서""" + + sample_activity_list = [ + { + "id": 1, + "title": "활동 제목 1", + "content": "월드 와이드 파이콘", + "activity_date": "2024-01-01", + }, + { + "id": 2, + "title": "활동 제목 2", + "content": "월드 와이드 파이콘2", + "activity_date": "2024-06-01", + }, + ] + + @classmethod + def list(cls): + """활동 히스토리 목록 조회 문서""" + responses = { + "성공": OpenApiResponse( + response=ListSuccessResponseSerializer, + description="활동 히스토리 목록 조회 성공", + examples=[ + OpenApiExample( + name="활동 히스토리 목록 조회", + value={ + "status": "SUCCESS", + "data": cls.sample_activity_list, + "error": None, + "pagination": {"count": 2, "next": None, "previous": None}, + }, + ), + OpenApiExample( + name="활동 히스토리 목록 조회 (데이터 없음)", + value={ + "status": "SUCCESS", + "data": [], + "error": None, + "pagination": {"count": 0, "next": None, "previous": None}, + }, + ), + ], + ), + "에러": OpenApiResponse(response=ErrorResponseSerializer, description="응답 에러"), + } + return cls.generate_schema( + operation_id="activity_action_list", + description="모든 활동 히스토리 목록 조회", + responses=responses, + ) diff --git a/src/activity_gallery/urls.py b/src/activity_gallery/urls.py index 7e69354..703a372 100644 --- a/src/activity_gallery/urls.py +++ b/src/activity_gallery/urls.py @@ -1,8 +1,9 @@ from rest_framework.routers import DefaultRouter -from .views import ActivityActionViewSet +from .views import ActivityActionViewSet, ActivityHistoryViewSet router = DefaultRouter() +router.register("histories", ActivityHistoryViewSet, basename="activity-history") router.register("", ActivityActionViewSet, basename="activity-action") urlpatterns = router.urls From a3982e123c9a89e2e5d04be20f98f02955010cc5 Mon Sep 17 00:00:00 2001 From: jungmir Date: Fri, 27 Jun 2025 01:04:32 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Feat:=20=ED=99=9C=EB=8F=99=20=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/activity_gallery/admin.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/activity_gallery/admin.py b/src/activity_gallery/admin.py index 5575b24..6a06f8e 100644 --- a/src/activity_gallery/admin.py +++ b/src/activity_gallery/admin.py @@ -3,7 +3,7 @@ from public.tag_models import ActivityTag, Tag -from .models import ActionPhoto, ActivityAction +from .models import ActionPhoto, ActivityAction, ActivityHistory class ActivityTagInline(admin.TabularInline): @@ -63,3 +63,20 @@ def delete_queryset(self, request, queryset): # Admin에서 여러 객체 삭제 시 호출됨 for obj in queryset: obj.delete() + + +@admin.register(ActivityHistory) +class ActivityHistoryAdmin(admin.ModelAdmin): + """활동 히스토리 관리자 페이지""" + + list_display = ("title", "activity_date") + search_fields = ("title", "content", "activity_date") + + def content_preview(self, obj): + """내용이 길 경우 일부만 미리보기""" + return obj.content[:50] + "..." if len(obj.content) > 50 else obj.content + + def delete_queryset(self, request, queryset): + # Admin에서 여러 객체 삭제 시 호출됨 + for obj in queryset: + obj.delete()