diff --git a/apps/posts/tasks.py b/apps/posts/tasks.py index 8930c0d..308eba0 100644 --- a/apps/posts/tasks.py +++ b/apps/posts/tasks.py @@ -68,13 +68,27 @@ def generate_feed_post(self, quest_id: str) -> None: if Post.objects.filter(quest=quest).exists(): return - Post.objects.create( + post = Post.objects.create( character=character, quest=quest, content=caption, img_url=img_url, ) + # 피드가 실제로 올라오면 작성 캐릭터의 주인에게 알림(토스트+알림창)을 남긴다. + # 알림 생성 실패가 피드 생성을 깨뜨리지 않도록 best-effort 로 처리한다. + try: + from apps.users.notification_service import create_feed_post_notification + + create_feed_post_notification( + user=character.user, + character_name=character.character_name, + caption=caption, + post_id=post.post_id, + ) + except Exception: + logger.warning("feed notification 생성 실패: quest_id=%s", quest_id) + @shared_task(bind=True, max_retries=2) def generate_character_reply(self, comment_id: str) -> None: diff --git a/apps/users/notification_service.py b/apps/users/notification_service.py index 2538a1c..6586484 100644 --- a/apps/users/notification_service.py +++ b/apps/users/notification_service.py @@ -1,11 +1,30 @@ from __future__ import annotations from datetime import date +import uuid from apps.todos.models import Reflection from apps.users.models import Notification, User REFLECTION_NOTIFICATION_TYPE = "reflection" +FEED_NOTIFICATION_TYPE = "feed" + + +def create_feed_post_notification( + *, user: User, character_name: str, caption: str, post_id: uuid.UUID | str +) -> Notification: + """피드(마을 게시판 글)가 생성되면 작성 캐릭터의 주인에게 알림을 남긴다. + + reflection 알림과 동일하게 Notification 한 건을 만들고, 프론트는 폴링으로 받아 + 토스트 + 알림창에 함께 띄운다. type/data 필드만 쓰므로 스키마 변경이 없다. + """ + return Notification.objects.create( + user=user, + type=FEED_NOTIFICATION_TYPE, + title=f"{character_name}의 새 소식이 도착했어요!", + content=caption, + data={"target": "feed", "post_id": str(post_id)}, + ) def create_reflection_notification( diff --git a/tests/test_posts.py b/tests/test_posts.py index dd481c8..75f6fc4 100644 --- a/tests/test_posts.py +++ b/tests/test_posts.py @@ -14,7 +14,7 @@ from apps.quests.models import Quest from apps.tags.models import Tag from apps.todos.models import Todo -from apps.users.models import TokenTransaction, User +from apps.users.models import Notification, TokenTransaction, User @pytest.mark.django_db @@ -50,6 +50,36 @@ def test_generate_feed_post_sends_appearance_payload( assert Post.objects.filter(quest=quest).exists() +@pytest.mark.django_db +def test_generate_feed_post_creates_feed_notification( + character: Character, quest: Quest, settings +) -> None: + # 피드가 생성되면 작성 캐릭터의 주인에게 type="feed" 알림이 남아야 한다. + settings.AI_SERVICE_URL = "http://ai.test" + settings.AI_SERVICE_TOKEN = "secret-key" # noqa: S105 + + response = MagicMock() + response.raise_for_status.return_value = None + response.json.return_value = { + "result": { + "image_url": "https://cdn.test/feed.png", + "caption": "오늘도 산책을 완료했어요.", + } + } + + with patch("apps.posts.tasks.httpx.post", return_value=response): + from apps.posts.tasks import generate_feed_post + + generate_feed_post.run(str(quest.quest_id)) + + post = Post.objects.get(quest=quest) + note = Notification.objects.get(user=character.user, type="feed") + assert character.character_name in note.title + assert note.content == "오늘도 산책을 완료했어요." + assert note.data["target"] == "feed" + assert note.data["post_id"] == str(post.post_id) + + def _make_other_user_post() -> Post: """다른 사용자 소유의 게시글을 하나 만든다(스코프 격리 검증용).""" other = User.objects.create_user(