Skip to content

feat: 세션 시간표 담기 API 작성 및 테스트 로직 추가#56

Merged
earthyoung merged 2 commits into
mainfrom
feature/add-my-schedule-timetable
Jul 11, 2026
Merged

feat: 세션 시간표 담기 API 작성 및 테스트 로직 추가#56
earthyoung merged 2 commits into
mainfrom
feature/add-my-schedule-timetable

Conversation

@earthyoung

Copy link
Copy Markdown
Contributor

구현한 배경입니다:

  • 프론트엔드에서 세션 시간표 담기/빼기 기능이 localStorage 목업으로 구현 완료된 상태인데요. 이를 실제 서버에 저장하기 위한 백엔드 API를 구현했습니다.
  • 요 문서에서 은지님과 스펙을 결정했고, 해당 부분을 구현했어요.

구현한 내용은 아래와 같습니다:

  • PresentationBookmark 모델 추가 (event.presentation app 에다가 추가해 놓았어요)
  • 북마크는 hard delete가 적합해 보여서 BaseAbstractModel은 사용하지 않았습니다..!
  • 같은 유저가 같은 세션에 2번 이상 Bookmark를 추가하면 중복이므로 (user, presentation)에 대해서 unique constraint를 걸어 두었어요.

작업한 API 엔드포인트들은 아래와 같습니다:

  • 요청한 유저의 현재 PresentationBookmark ID 목록 조회
  • 요청한 유저에 대해서, 해당 세션을 PresentationBookmark로 추가
  • 요청한 유저에 대해서, 해당 세션을 PresentationBookmark에서 제거
  • 이때 POST, DELETE 요청은 멱등하게(여러 번 요청해도 결과가 같도록) 구현했습니다.
  • 관련 테스트를 추가로 작성했습니다.

Comment thread app/event/presentation/views.py Outdated
Comment on lines +45 to +56
def _resolve_event(self, request: Request) -> Event:
event_name = request.query_params.get("event")
if not event_name:
event = Event.objects.filter_active().first()
if not event:
raise NotFound("해당 행사 정보가 없습니다.")
return event

event = Event.objects.filter_active().filter(Q(name_ko=event_name) | Q(name_en=event_name)).first()
if not event:
raise NotFound("해당 행사 정보가 없습니다.")
return event

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filterset_class나 filterset_fields 사용은 어떠신가요?


@extend_schema(
summary="북마크 목록 조회",
parameters=[],

@MU-Software MU-Software Jul 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

결과적으로는 event라는 parameter를 받는 것 아닐까 싶어요!
추가로, 아마 filterset_class나 filterset_fields를 사용하시면 자동으로 문서에 정의될 것 같아요...?

Comment thread app/event/presentation/views.py Outdated
Comment on lines +69 to +73
presentation_ids = list(
PresentationBookmark.objects.filter(user=request.user, event=event).values_list(
"presentation_id", flat=True
)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 위의 filterset_class나 filterset_fields을 사용하고
  • queryset은 PresentationBookmark.objects.all() 로 정의한 후
  • get_queryset에서 super().get_queryset().filter(user=request.user)로 정의하면
    단순하게 list(self.get_queryset().values_list("presentation_id", flat=True)) 로 요약 가능할 것 같아요!

Comment thread app/event/presentation/serializers.py Outdated


class PresentationBookmarkRequestSerializer(serializers.Serializer):
presentation_id = serializers.UUIDField()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serializers.PrimaryKeyRelatedField를 사용하시면, 나중에 create 메소드에서 한번 더 쿼리하실 필요 없을 것 같아요!

Comment thread app/event/presentation/views.py Outdated
Comment on lines +91 to +93
presentation = Presentation.objects.filter_active().filter(id=presentation_id).first()
if not presentation:
raise NotFound("해당 세션 정보가 없습니다.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에서 언급한대로 serializers.PrimaryKeyRelatedField 한번 추천 드립니다!
추가로 walrus는 어떠신지요ㅎㅎ

Comment thread app/event/presentation/views.py Outdated
Comment on lines +95 to +100
event = presentation.type.event
_, created = PresentationBookmark.objects.get_or_create(
user=request.user,
presentation=presentation,
defaults={"event": event},
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serializer에서 create 메소드를 정의하고, save 메소드를 호출해서 생성되게 하는건 어때요?
추가로 serializer 내에서 self.context["request"].user가 가능할거에요 아마.

Comment thread app/event/presentation/views.py Outdated
Comment on lines +114 to +120
def destroy(self, request: Request, pk: str = None) -> Response:
presentation = Presentation.objects.filter_active().filter(id=pk).first()
if not presentation:
raise NotFound("해당 세션 정보가 없습니다.")

PresentationBookmark.objects.filter(user=request.user, presentation=presentation).delete()
return Response(status=status.HTTP_204_NO_CONTENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_queryset() + mixins.DestroyModelMixin 조합을 사용하시면 요 destroy 메소드는 필요없을수도 있을 것 같슴다.

filterset_class = PresentationFilterSet


@extend_schema(tags=[OpenAPITag.EVENT_PRESENTATION_BOOKMARK])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요게 GenericViewSet 클래스의 모든 view 메소드에 tags를 적용시켜주는지는 잘 모르겠네요 🤔

Comment thread app/event/presentation/views.py Outdated
Comment on lines +19 to +23
from rest_framework import mixins, status, viewsets
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from rest_framework import mixins, status, viewsets
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import exceptions, mixins, permissions, request, response, status, viewsets

Suggested change
from rest_framework import mixins, status, viewsets
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.exceptions import NotFound
from rest_framework.mixins import ...
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.status import ...
from rest_framework.viewsets import GenericViewSet

로 통일하는건 어떨까요?

Comment thread app/event/presentation/models.py Outdated
class PresentationBookmark(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="presentation_bookmarks")
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="presentation_bookmarks")

@MU-Software MU-Software Jul 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에 event를 두신건 빠른 쿼리를 위해서일까요? presentation__type__event로도 쿼리는 가능할 것 같아서요!
그리고 앞서 말씀드린 filterset_class에선 아래처럼 정의하고요.

class PresentationBookmark(filters.FilterSet):
    event_id = filters.UUIDFilter(field_name="presentation__type__event_id")
    event_name = filters.CharFilter(field_name="presentation__type__event__name", lookup_expr="icontains")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 감사합니다. 이렇게 바꿔볼게요!

Comment thread app/event/presentation/urls.py Outdated
bookmark_router.register("bookmarks", views.PresentationBookmarkViewSet, basename="presentation-bookmark")

urlpatterns = [
path("", include(bookmark_router.urls)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 최종 경로가 어떻게 될까요?

rest-api.pycon.kr/v1/bookmarks/

라면, 혹시 presentation의 bookmark라는 것을 명시적으로 하기 위해 (+ 기존 공개용 API가 복수형을 안 쓰던 컨벤션을 감안하여)

rest-api.pycon.kr/v1/event/presentation-bookmark/

는 어떠실지 권장드립니다!

@MU-Software MU-Software left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

경로만 제외하면 대부분 nitpick 성입니다, 적절히 고려 부탁드려요~

@earthyoung earthyoung merged commit 833c91c into main Jul 11, 2026
3 checks passed
@earthyoung earthyoung deleted the feature/add-my-schedule-timetable branch July 11, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants