diff --git a/accounts/models.py b/accounts/models.py index c308052..80206c7 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -99,6 +99,6 @@ def __str__(self): return self.email def get_image_url(self): - if getattr(self, "image", None): - return self.image.image_url + if hasattr(self, "image") and hasattr(self.image, "url"): + return self.image.url return "https://paullab.co.kr/images/weniv-licat.png" diff --git a/courses/models.py b/courses/models.py index e373f7c..cf4113f 100644 --- a/courses/models.py +++ b/courses/models.py @@ -113,8 +113,8 @@ class Course(models.Model): updated_at = models.DateTimeField(auto_now=True, verbose_name="수정일") def get_thumbnail(self): - if hasattr(self, "images") and self.images.exists(): - return self.images.first().file.url + if hasattr(self, "image"): + return self.image.url return "https://www.gravatar.com/avatar/205e460b479e2e5b48aec077" def update(self, **kwargs): diff --git a/courses/serializers.py b/courses/serializers.py index 1102366..36881b6 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -89,7 +89,7 @@ class Meta: def get_video_url(self, obj): if getattr(obj, "video", None): - return obj.video.video_url + return obj.video.url return None def get_video_duration(self, obj): @@ -164,9 +164,8 @@ class Meta: ] def get_author_image(self, obj): - print(obj.author.image.image_url) if getattr(obj.author, "image", None): - return obj.author.image.image_url + return obj.author.image.url return None def get_author_name(self, obj): @@ -174,7 +173,7 @@ def get_author_name(self, obj): def get_video_url(self, obj): if getattr(obj, "video", None): - return obj.video.video_url + return obj.video.url return None def get_thumbnail_url(self, obj): @@ -233,6 +232,8 @@ def get_thumbnail(self, obj): return obj.get_thumbnail() def get_author_image(self, obj): + if getattr(obj.author, "image", None): + return obj.author.image.url return "https://paullab.co.kr/images/weniv-licat.png" def get_author_name(self, obj): @@ -314,6 +315,8 @@ class Meta: ] def get_author_image(self, obj): + if getattr(obj.author, "image", None): + return obj.author.image.url return "https://paullab.co.kr/images/weniv-licat.png" def get_author_name(self, obj): diff --git a/courses/test/conftest.py b/courses/test/conftest.py index 728d30a..d9ca727 100644 --- a/courses/test/conftest.py +++ b/courses/test/conftest.py @@ -136,7 +136,7 @@ def create_user(): user = User.objects.create_user( email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD, nickname="testuser" ) - Image.objects.create(user=user, image_url="test.jpg") + Image.objects.create(user=user, url="test.jpg") return user @@ -148,7 +148,7 @@ def create_staff_user(): is_staff=True, nickname="staffuser", ) - Image.objects.create(user=user, image_url="test.jpg") + Image.objects.create(user=user, url="test.jpg") return user diff --git a/materials/migrations/0009_remove_image_image_url_remove_video_video_url.py b/materials/migrations/0009_remove_image_image_url_remove_video_video_url.py new file mode 100644 index 0000000..a41ebb7 --- /dev/null +++ b/materials/migrations/0009_remove_image_image_url_remove_video_video_url.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.1 on 2024-10-14 06:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('materials', '0008_alter_image_options_alter_video_options_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='image', + name='image_url', + ), + migrations.RemoveField( + model_name='video', + name='video_url', + ), + ] diff --git a/materials/migrations/0010_image_url_video_url.py b/materials/migrations/0010_image_url_video_url.py new file mode 100644 index 0000000..4565bdd --- /dev/null +++ b/materials/migrations/0010_image_url_video_url.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.1 on 2024-10-14 06:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materials', '0009_remove_image_image_url_remove_video_video_url'), + ] + + operations = [ + migrations.AddField( + model_name='image', + name='url', + field=models.URLField(default='https://paullab.co.kr/images/weniv-licat.png', verbose_name='이미지 URL'), + ), + migrations.AddField( + model_name='video', + name='url', + field=models.URLField(default='https://www.youtube.com/watch?v=bZh8oUIDfdI&t=1s', verbose_name='동영상 URL'), + ), + ] diff --git a/materials/models.py b/materials/models.py index dffc7de..44c0132 100644 --- a/materials/models.py +++ b/materials/models.py @@ -1,8 +1,9 @@ -from accounts.models import CustomUser -from courses.models import Course, Topic from django.conf import settings from django.db import models +from accounts.models import CustomUser +from courses.models import Course, Topic + class Image(models.Model): """ @@ -34,9 +35,9 @@ class Image(models.Model): null=True, blank=True, ) - - image_url = models.ImageField( - upload_to="images/", blank=True, null=True, verbose_name="이미지 파일" + url = models.URLField( + verbose_name="이미지 URL", + default="https://paullab.co.kr/images/weniv-licat.png", ) is_deleted = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) @@ -54,10 +55,10 @@ def __str__(self): def save(self, *args, **kwargs): - if self.user and not self.image_url: - self.image_url = f"{settings.MEDIA_URL}images/default_user_image.jpg" - if self.course and not self.image_url: - self.image_url = f"{settings.MEDIA_URL}images/default_user_image.jpg" + if self.user and not self.url: + self.url = f"{settings.MEDIA_URL}images/default_user_image.jpg" + if self.course and not self.url: + self.url = f"{settings.MEDIA_URL}images/default_user_image.jpg" super().save(*args, **kwargs) @@ -75,8 +76,9 @@ class Video(models.Model): course = models.OneToOneField( Course, on_delete=models.CASCADE, related_name="video", null=True, blank=True ) - video_url = models.FileField( - upload_to="videos/", blank=True, null=True, verbose_name="비디오 파일" + url = models.URLField( + verbose_name="동영상 URL", + default="https://www.youtube.com/watch?v=bZh8oUIDfdI&t=1s", ) is_deleted = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) @@ -93,8 +95,8 @@ def __str__(self): return "Video" def save(self, *args, **kwargs): - if not self.video_url: - self.video_url = f"{settings.MEDIA_URL}videos/default_video.mp4" + if not self.url: + self.url = f"{settings.MEDIA_URL}videos/default_video.mp4" super().save(*args, **kwargs) diff --git a/materials/serializers.py b/materials/serializers.py index 7d2c24b..6b547e7 100644 --- a/materials/serializers.py +++ b/materials/serializers.py @@ -12,18 +12,21 @@ class ImageSerializer(serializers.ModelSerializer): - 검사: 파일 형식과 손상 여부에 대해 유효성 검사를 합니다. """ + file = serializers.ImageField(write_only=True) + class Meta: model = Image fields = [ "id", - "image_url", + "url", "is_deleted", "created_at", "updated_at", + "file", ] read_only_fields = [ "id", - "image_url", + "url", "is_deleted", "created_at", "updated_at", @@ -31,11 +34,11 @@ class Meta: def validate_file(self, value): allowed_image_extensions = (".png", ".jpg", ".jpeg") - if not value.name.endswith(allowed_image_extensions): raise serializers.ValidationError( "지원하지 않는 파일 형식입니다. PNG, JPG, JPEG만 가능합니다." ) + try: img = PILImage.open(value) img.verify() @@ -51,44 +54,47 @@ class VideoSerializer(serializers.ModelSerializer): - 검사: 파일 형식과 손상 여부에 대해 유효성 검사를 합니다. """ + file = serializers.FileField(write_only=True) + class Meta: model = Video fields = [ "id", - "video_url", + "url", + "file", "is_deleted", "created_at", "updated_at", ] - read_only_fields = ["id", "video_url", "is_deleted", "created_at", "updated_at"] + read_only_fields = ["id", "url", "is_deleted", "created_at", "updated_at"] def validate_file(self, value): allowed_extensions = ["mp4", "avi", "mov", "wmv"] - if not value.name.split(".")[-1] in allowed_extensions: + if value.name.split(".")[-1] not in allowed_extensions: raise serializers.ValidationError( f"허용되지 않는 파일 형식입니다. 다음 형식만 가능합니다: {', '.join(allowed_extensions)}." ) - try: - cap = cv2.VideoCapture(value) - if not cap.isOpened(): - raise serializers.ValidationError( - "비디오 파일을 열 수 없습니다. 파일이 손상되었을 수 있습니다." - ) - - ret, frame = cap.read() - if not ret: - raise serializers.ValidationError( - "비디오 파일을 읽을 수 없습니다. 파일이 손상되었을 수 있습니다." - ) - - except Exception as e: - raise serializers.ValidationError( - f"비디오 파일 검사 중 오류가 발생했습니다: {str(e)}" - ) - finally: - cap.release() + # try: + # cap = cv2.VideoCapture(value) + # if not cap.isOpened(): + # raise serializers.ValidationError( + # "비디오 파일을 열 수 없습니다. 파일이 손상되었을 수 있습니다." + # ) + + # ret, frame = cap.read() + # if not ret: + # raise serializers.ValidationError( + # "비디오 파일을 읽을 수 없습니다. 파일이 손상되었을 수 있습니다." + # ) + + # except Exception as e: + # raise serializers.ValidationError( + # f"비디오 파일 검사 중 오류가 발생했습니다: {str(e)}" + # ) + # finally: + # cap.release() return value diff --git a/materials/views.py b/materials/views.py index 1114700..9d8ad28 100644 --- a/materials/views.py +++ b/materials/views.py @@ -1,9 +1,8 @@ import io +import time import boto3 import ffmpeg -from accounts.models import CustomUser -from accounts.permissions import IsSuperUser, IsTutor from botocore.exceptions import ClientError from django.conf import settings from django.contrib.auth import get_user_model @@ -15,6 +14,9 @@ from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.response import Response +from accounts.models import CustomUser +from accounts.permissions import IsSuperUser, IsTutor + from .models import Image, Video, VideoEventData from .serializers import ImageSerializer, VideoEventDataSerializer, VideoSerializer @@ -41,22 +43,6 @@ def optimize_image(image_file): return optimized_io -def optimize_video(video_file): - """ - 동영상 파일을 최적화합니다. - - 포맷 변환 - - 리사이징 작업 - """ - output_io = io.BytesIO() - - ffmpeg.input(video_file).output( - output_io, format="mp4", video_bitrate="1000k", s="640x360", preset="fast" - ).run(overwrite_output=True) - - output_io.seek(0) - return output_io - - def upload_to_s3(file_io, file_name, content_type): """ 파일을 S3에 업로드합니다. @@ -92,8 +78,7 @@ class ImageCreateView(generics.CreateAPIView): def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - - image_file = request.FILES.get("image_url") + image_file = request.FILES.get("file") if not image_file: return Response( @@ -102,10 +87,8 @@ def create(self, request, *args, **kwargs): ) optimized_image = optimize_image(image_file) - try: - - user = get_object_or_404(CustomUser, id=request.data.get("user_id")) + user = get_object_or_404(CustomUser, id=request.user.id) if user: timestamp = int(time.time()) @@ -120,7 +103,7 @@ def create(self, request, *args, **kwargs): file_url = f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/{file_name}" - image = serializer.save(image_url=file_url) + image = Image.objects.create(author=user, url=file_url) return Response( self.get_serializer(image).data, status=status.HTTP_201_CREATED @@ -202,7 +185,7 @@ def put(self, request, *args, **kwargs): file_url = f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/{file_name}" - image = serializer.save(image_url=file_url) + image = serializer.save(url=file_url) return Response(self.get_serializer(image).data, status=status.HTTP_200_OK) except ClientError as e: @@ -255,28 +238,25 @@ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - video_file = request.FILES.get("video_url") + video_file = request.FILES.get("file") if not video_file: return Response( {"error": "동영상 파일이 필요합니다."}, status=status.HTTP_400_BAD_REQUEST, ) - optimized_video = self.optimize_video(video_file) - try: - user = get_object_or_404(CustomUser, id=request.data.get("user_id")) + user = get_object_or_404(CustomUser, id=request.user.id) if user: timestamp = int(time.time()) file_name = f"videos/user_{user.id}/{timestamp}_{video_file.name}" - upload_to_s3(optimized_video, file_name, "video/mp4") + upload_to_s3(video_file, file_name, "video/mp4") file_url = f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/{file_name}" - serializer.validated_data["video_url"] = file_url - video = serializer.save(video_url=file_url) + video = Video.objects.create(url=file_url) return Response( self.get_serializer(video).data, status=status.HTTP_201_CREATED @@ -285,8 +265,6 @@ def create(self, request, *args, **kwargs): return Response( {"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - finally: - del optimized_video class VideoListView(generics.ListAPIView): @@ -324,7 +302,7 @@ def put(self, request, *args, **kwargs): serializer = self.get_serializer(video, data=request.data, partial=True) serializer.is_valid(raise_exception=True) - video_file = request.FILES.get("video_url") + video_file = request.FILES.get("file") if not video_file: return Response( {"error": "동영상 파일이 필요합니다."}, @@ -343,7 +321,7 @@ def put(self, request, *args, **kwargs): ) s3_client.delete_object( Bucket=settings.AWS_STORAGE_BUCKET_NAME, - Key=video.video_url.split("/")[-1], + Key=video.url.split("/")[-1], ) user = get_object_or_404(CustomUser, id=request.data.get("user_id")) @@ -356,7 +334,7 @@ def put(self, request, *args, **kwargs): file_url = f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/{file_name}" - serializer.validated_data["video_url"] = file_url + serializer.validated_data["url"] = file_url video = serializer.save() return Response(self.get_serializer(video).data, status=status.HTTP_200_OK) @@ -383,8 +361,8 @@ def delete(self, request, *args, **kwargs): ) try: - image.is_deleted = True - image.save() + video.is_deleted = True + video.save() return Response(status=status.HTTP_204_NO_CONTENT) except ClientError as e: return Response( diff --git a/payments/admin.py b/payments/admin.py index d9b63b3..a08cb3c 100644 --- a/payments/admin.py +++ b/payments/admin.py @@ -1,5 +1,8 @@ from django.contrib import admin -from .models import Cart, Order, Payment, UserBillingAddress + +from .models import Cart, Order, OrderItem, Payment, UserBillingAddress + +admin.site.register(OrderItem) @admin.register(Cart) diff --git a/payments/mixins.py b/payments/mixins.py index 63553f5..8818316 100644 --- a/payments/mixins.py +++ b/payments/mixins.py @@ -74,8 +74,8 @@ def create_order_from_cart(self, user, cart): order_items = [ { - "curriculum_id": item.curriculum.id if item.curriculum else None, - "course_id": item.course.id if item.course else None, + "curriculum": item.curriculum.id if item.curriculum else None, + "course": item.course.id if item.course else None, "quantity": item.quantity, "price": item.get_price(), } diff --git a/payments/models.py b/payments/models.py index f9a97ab..816928a 100644 --- a/payments/models.py +++ b/payments/models.py @@ -1,8 +1,8 @@ +from django.conf import settings from django.db import models from django.utils import timezone -from django.conf import settings -from courses.models import Curriculum, Course +from courses.models import Course, Curriculum class Cart(models.Model): @@ -93,10 +93,10 @@ def get_price(self): return unit_price * self.quantity def get_image_url(self): - if self.curriculum and hasattr(self.curriculum, "images"): - return self.curriculum.images.url - elif self.course and hasattr(self.course, "images"): - return self.course.images.url + if self.curriculum and hasattr(self.curriculum, "image"): + return self.curriculum.image.url + elif self.course and hasattr(self.course, "image"): + return self.course.image.url return None class Meta: @@ -222,10 +222,10 @@ def get_price(self): return unit_price * self.quantity def get_image_url(self): - if self.curriculum and hasattr(self.curriculum, "images"): - return self.curriculum.images.url - elif self.course and hasattr(self.course, "images"): - return self.course.images.url + if self.curriculum and hasattr(self.curriculum, "image"): + return self.curriculum.image.url + elif self.course and hasattr(self.course, "image"): + return self.course.image.url return None def set_expiry_date(self): diff --git a/payments/serializers.py b/payments/serializers.py index a5d5870..70da6c8 100644 --- a/payments/serializers.py +++ b/payments/serializers.py @@ -75,6 +75,8 @@ class OrderItemSerializer(serializers.ModelSerializer): 주문 상품 모델의 시리얼라이저입니다. """ + thumbnail = serializers.SerializerMethodField() + class Meta: model = OrderItem fields = [ @@ -89,14 +91,30 @@ class Meta: "get_item_name", "get_price", "get_image_url", + "thumbnail", ] read_only_fields = [ "id", "quantity", "created_at", "updated_at", + "thumbnail", ] + def get_thumbnail(self, obj): + if ( + obj.curriculum + and hasattr(obj.curriculum, "image") + and hasattr(obj.curriculum.image, "url") + ): + return obj.curriculum.image.url + if ( + obj.course + and hasattr(obj.course, "image") + and hasattr(obj.course.image, "url") + ): + return obj.course.image.url + def validate(self, data): # 커리큘럼과 코스 중 하나만 선택되었는지 확인합니다. # curriculum = data.get("curriculum") diff --git a/weaverse/settings.py b/weaverse/settings.py index 0741d9a..33b0f5d 100644 --- a/weaverse/settings.py +++ b/weaverse/settings.py @@ -149,13 +149,8 @@ ] # CORS 설정 -if DEBUG: - CORS_ALLOWED_ORIGINS = [ - "https://www.weaverse.site", # 프로덕션 환경 - "http://localhost:3000", # 개발 환경 프론트엔드 - ] -else: - CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS", "").split(",") + +CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS", "").split(",") DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"