Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/chat/services/chatroom_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_chatrooms(user: User) -> list[dict[str, Any]]:
# 채팅방 목록 조회
memberships = GroupMember.objects.filter(user_id=user.id).select_related("study_group_id")

groups = [m.study_group_id for m in memberships]
groups = [m.study_group for m in memberships]
group_ids = [g.id for g in groups]

if not group_ids:
Expand Down Expand Up @@ -100,7 +100,7 @@ def get_room_info(study_group: StudyGroup, user: User) -> dict[str, Any]:

member_list = [
{
"nickname": m.user_id.nickname,
"nickname": m.user.nickname,
"is_leader": m.is_leader,
}
for m in members
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.8 on 2025-12-03 01:48

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("study_groups", "0005_review"),
]

operations = [
migrations.RenameField(
model_name="groupmember",
old_name="study_group_id",
new_name="study_group",
),
migrations.RenameField(
model_name="groupmember",
old_name="user_id",
new_name="user",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.8 on 2025-12-03 01:54

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("study_groups", "0006_rename_study_group_id_groupmember_study_group_and_more"),
]

operations = [
migrations.RenameField(
model_name="studylecture",
old_name="lecture_id",
new_name="lecture",
),
migrations.RenameField(
model_name="studylecture",
old_name="study_group_id",
new_name="study_group",
),
]
4 changes: 2 additions & 2 deletions apps/study_groups/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

__all__ = [
"StudyGroup",
"GroupMember",
"StudyLecture",
"StudyNote",
"StudyNoteImage",
"StudyNoteAttachment",
"GroupMember",
"StudyLecture",
"GroupSchedule",
"ScheduleParticipants",
"Review",
Expand Down
10 changes: 5 additions & 5 deletions apps/study_groups/models/study_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,20 @@ def __str__(self) -> str:

# 그룹 멤버
class GroupMember(TimeStampedModel):
study_group_id = models.ForeignKey("study_groups.StudyGroup", on_delete=models.CASCADE)
user_id = models.ForeignKey(User, on_delete=models.CASCADE)
study_group = models.ForeignKey("study_groups.StudyGroup", on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
is_leader = models.BooleanField(default=False)

class Meta:
db_table = "study_members"

def __str__(self) -> str:
return f"{self.study_group_id.name}의 멤버 {self.user_id.nickname}"
return f"{self.study_group_id}의 멤버 {self.user_id}"


class StudyLecture(TimeStampedModel):
lecture_id = models.ForeignKey("lectures.CrawledLecture", on_delete=models.CASCADE)
study_group_id = models.ForeignKey("study_groups.StudyGroup", on_delete=models.CASCADE)
lecture = models.ForeignKey("lectures.CrawledLecture", on_delete=models.CASCADE)
study_group = models.ForeignKey("study_groups.StudyGroup", on_delete=models.CASCADE)

class Meta:
db_table = "study_lectures"
5 changes: 5 additions & 0 deletions apps/study_groups/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .study_group import StudyGroupSerializer

__all__ = [
"StudyGroupSerializer",
]
142 changes: 142 additions & 0 deletions apps/study_groups/serializers/study_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from datetime import datetime
from typing import Any

from rest_framework import serializers

from apps.lectures.models import CrawledLecture
from apps.study_groups.models import StudyGroup, StudyLecture


# 스터디그룹
class StudyGroupSerializer(serializers.ModelSerializer[StudyGroup]):
# 강의 필드 임의 추가
lectures = serializers.ListField(
child=serializers.IntegerField(min_value=1),
required=False,
help_text="Lecture 목록(최대 5개)",
max_length=5,
)

class Meta:
model = StudyGroup
fields = [
"id",
"name",
"introduction",
"max_headcount",
"profile_img_url",
"start_at",
"end_at",
"status",
"lectures",
]
read_only_fields = ["id", "status"]

# 유효성 관련
def validate_start_at(self, value: datetime) -> datetime:
if value.date() < datetime.now().date():
raise serializers.ValidationError("스터디 시작일은 오늘 이후여야 합니다.")
return value

def validate_end_at(self, value: datetime) -> datetime:
start_at = self.initial_data.get("start_at")
if start_at:
start_date = datetime.fromisoformat(start_at).date()
if (value.date() - start_date).days < 5:
raise serializers.ValidationError("스터디 종료일은 시작일보다 최소 5일 이후여야 합니다.")
return value

def validate_lectures(self, value: list[int]) -> list[int]:
if len(value) > 5:
raise serializers.ValidationError("강의는 최대 5개까지 선택 가능합니다.")
return value

def create(self, validated_data: dict[str, Any]) -> StudyGroup:
lectures_data = validated_data.pop("lectures", [])
study_group = StudyGroup.objects.create(**validated_data)

for lecture_id in lectures_data:
StudyLecture.objects.create(
study_group_id=study_group, lecture_id=CrawledLecture.objects.get(id=lecture_id)
)
return study_group

def update(self, instance: StudyGroup, validated_data: dict[str, Any]) -> StudyGroup:
lectures_data = validated_data.pop("lectures", None)

for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()

if lectures_data is not None:
StudyLecture.objects.filter(study_group_id=instance).delete()
for lecture_id in lectures_data:
StudyLecture.objects.create(
study_group_id=instance, lecture_id=CrawledLecture.objects.get(id=lecture_id)
)
return instance


class StudyGroupListSerializer(serializers.ModelSerializer[StudyGroup]):
lectures = serializers.SerializerMethodField()
member_count = serializers.SerializerMethodField()
is_leader = serializers.SerializerMethodField()

class Meta:
model = StudyGroup
fields = [
"id",
"name",
"member_count",
"is_leader",
"profile_img_url",
"start_at",
"end_at",
"status",
"lectures",
]

def get_lectures(self, obj: StudyGroup) -> list[dict[str, Any]]:
return [{"title": sl.lecture.title, "instructor": sl.lecture.instructor} for sl in obj.studylecture_set.all()]

def get_member_count(self, obj: StudyGroup) -> str:
return f"{obj.groupmember_set.count()} / {obj.max_headcount}"

def get_is_leader(self, obj: StudyGroup) -> bool:
user = self.context["request"].user
return obj.groupmember_set.filter(user_id=user, is_leader=True).exists()


# 상세정보 조회용
class StudyGroupDetailSerializer(serializers.ModelSerializer[StudyGroup]):
lectures = serializers.SerializerMethodField()
members = serializers.SerializerMethodField()

class Meta:
model = StudyGroup
fields = [
"id",
"name",
"introduction",
"profile_img_url",
"start_at",
"end_at",
"status",
"lectures",
"members",
]

def get_lectures(self, obj: StudyGroup) -> list[dict[str, Any]]:
return [
{
"thumbnail": sl.lecture.thumbnail_img_url,
"title": sl.lecture.title,
"instructor": sl.lecture.instructor,
"url": sl.lecture.url_link,
}
for sl in obj.studylecture_set.all()
]

def get_members(self, obj: StudyGroup) -> list[dict[str, Any]]:
members = obj.groupmember_set.all().order_by("-is_leader")
return [{"nickname": m.user_id, "is_leader": m.is_leader} for m in members]
11 changes: 11 additions & 0 deletions apps/study_groups/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter

from apps.study_groups.views.study_groups_view import StudyGroupViewSet

router = DefaultRouter()
router.register(r"study_groups", StudyGroupViewSet, basename="study_groups")

urlpatterns = [
path("/", include(router.urls)), # router URL 포함
]
5 changes: 5 additions & 0 deletions apps/study_groups/views/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .study_groups_view import StudyGroupViewSet

__all__ = [
"StudyGroupViewSet",
]
87 changes: 87 additions & 0 deletions apps/study_groups/views/study_groups_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from typing import Any, Type

from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
from rest_framework import viewsets
from rest_framework.serializers import Serializer

from apps.study_groups.models import StudyGroup
from apps.study_groups.serializers.study_group import (
StudyGroupDetailSerializer,
StudyGroupListSerializer,
StudyGroupSerializer,
)


# CRUD specAPI
@extend_schema_view(
create=extend_schema(
summary="스터디 그룹 생성 REQ-STDY-0001(POST)",
description="스터디 그룹 생성",
request=StudyGroupSerializer,
responses={
201: StudyGroupSerializer,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
},
tags=["StudyGroups"],
),
list=extend_schema(
summary="스터디 그룹 목록 조회 REQ-STDY-0002(GET)",
description="스터디 그룹 목록 조회",
responses=StudyGroupListSerializer,
tags=["StudyGroups"],
parameters=[
OpenApiParameter(
name="status",
type=OpenApiTypes.STR,
description="스터디 그룹 목록 조회",
required=False,
enum=["PENDING", "ONGOING", "ENDED"],
default="PENDING",
),
],
),
retrieve=extend_schema(
summary="스터디 그룹 상세 조회 REQ-STDY-0003(GET)",
description="스터디 그룹 상세 조회",
responses={
200: StudyGroupDetailSerializer,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
},
tags=["StudyGroups"],
),
update=extend_schema(
summary="스터디 그룹 수정 REQ-STDY-0004(PATCH)",
description="스터디 그룹 수정",
request=StudyGroupSerializer,
responses={
200: StudyGroupSerializer,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
},
tags=["StudyGroups"],
),
### patch로 자동 생성된 partial_update 쪽에도 summary 추가 및 스터디그룹 태그 포함하기 위해 작성
partial_update=extend_schema(
summary="스터디 그룹 수정 REQ-STDY-0004(PATCH)",
tags=["StudyGroups"],
),
destroy=extend_schema(
summary="스터디 그룹 삭제 REQ-STDY-0005(DELETE)",
description="스터디 그룹 삭제",
responses={200: OpenApiTypes.OBJECT},
tags=["StudyGroups"],
),
)
class StudyGroupViewSet(viewsets.ModelViewSet[StudyGroup]):
queryset = StudyGroup.objects.all()
serializer_class = StudyGroupSerializer

def get_serializer_class(self) -> Type[Serializer[Any]]:
if self.action == "list":
return StudyGroupListSerializer
if self.action == "retrieve":
return StudyGroupDetailSerializer
return StudyGroupSerializer
3 changes: 0 additions & 3 deletions apps/study_groups/views/views.py

This file was deleted.

1 change: 1 addition & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

urlpatterns: list[URLPattern | URLResolver] = [
path("api/v1", include("apps.lectures.urls")),
path("api/v1", include("apps.study_groups.urls")),
path("api/v1/notifications", include("apps.notification.urls", "notification")),
path("api/v1/", include("apps.application.urls")),
]
Expand Down
Loading