diff --git a/core/views.py b/core/views.py index 91ea44a2..686f110f 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,20 @@ from django.shortcuts import render -# Create your views here. +from rest_framework import status +from rest_framework.response import Response + + +def get_paginated_response(*, pagination_class, serializer_class, queryset, request, view): + paginator = pagination_class() + + page = paginator.paginate_queryset(queryset, request, view=view) + + if page is not None: + serializer = serializer_class(page, many=True, context={'request': request}) + else: + serializer = serializer_class(queryset, many=True, context={'request': request}) + + return Response({ + 'status': 'success', + 'data': paginator.get_paginated_response(serializer.data).data, + }, status=status.HTTP_200_OK) diff --git a/products/admin.py b/products/admin.py index 09b66c52..941c670c 100644 --- a/products/admin.py +++ b/products/admin.py @@ -1,7 +1,40 @@ from django.contrib import admin from . import models +@admin.register(models.Category) +class Category(admin.ModelAdmin): + """ """ + + pass + +@admin.register(models.Fit) +class Fit(admin.ModelAdmin): + + """ """ + + pass + +@admin.register(models.Texture) +class Texture(admin.ModelAdmin): + + """ """ + + pass + +@admin.register(models.Style) +class Style(admin.ModelAdmin): + + """ """ + + pass + +@admin.register(models.Detail) +class Detail(admin.ModelAdmin): + + """ """ + + pass @admin.register(models.Product) class Product(admin.ModelAdmin): diff --git a/products/migrations/0006_product_like_cnt_product_likeuser_set.py b/products/migrations/0006_product_like_cnt_product_likeuser_set.py new file mode 100644 index 00000000..b45f5178 --- /dev/null +++ b/products/migrations/0006_product_like_cnt_product_likeuser_set.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0 on 2024-01-24 11:59 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('products', '0005_remove_product_category_remove_product_like_cnt_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='like_cnt', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='product', + name='likeuser_set', + field=models.ManyToManyField(blank=True, related_name='liked_products', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/products/migrations/0007_category_fit_style_texture_product_category_and_more.py b/products/migrations/0007_category_fit_style_texture_product_category_and_more.py new file mode 100644 index 00000000..63cdeff4 --- /dev/null +++ b/products/migrations/0007_category_fit_style_texture_product_category_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 4.0 on 2024-01-27 01:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0006_product_like_cnt_product_likeuser_set'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Fit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Style', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Texture', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.AddField( + model_name='product', + name='category', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.category'), + preserve_default=False, + ), + migrations.AddField( + model_name='product', + name='fit', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.fit'), + ), + migrations.AddField( + model_name='product', + name='style', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.style'), + ), + migrations.AddField( + model_name='product', + name='texture', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.texture'), + ), + ] diff --git a/products/migrations/0008_alter_product_category.py b/products/migrations/0008_alter_product_category.py new file mode 100644 index 00000000..e54e9d34 --- /dev/null +++ b/products/migrations/0008_alter_product_category.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0 on 2024-01-27 02:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0007_category_fit_style_texture_product_category_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='category', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.category'), + ), + ] diff --git a/products/migrations/0009_option_alter_product_category_alter_product_option.py b/products/migrations/0009_option_alter_product_category_alter_product_option.py new file mode 100644 index 00000000..22413e50 --- /dev/null +++ b/products/migrations/0009_option_alter_product_category_alter_product_option.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0 on 2024-01-27 05:23 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0008_alter_product_category'), + ] + + operations = [ + migrations.CreateModel( + name='Option', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('price', models.CharField(blank=True, max_length=100)), + ], + ), + migrations.AlterField( + model_name='product', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.category'), + ), + migrations.AlterField( + model_name='product', + name='option', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.option'), + ), + ] diff --git a/products/migrations/0010_alter_product_option_delete_option.py b/products/migrations/0010_alter_product_option_delete_option.py new file mode 100644 index 00000000..a84da98f --- /dev/null +++ b/products/migrations/0010_alter_product_option_delete_option.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0 on 2024-01-27 05:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0009_option_alter_product_category_alter_product_option'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='option', + field=models.TextField(blank=True), + ), + migrations.DeleteModel( + name='Option', + ), + ] diff --git a/products/migrations/0011_remove_product_fit_product_fit_remove_product_style_and_more.py b/products/migrations/0011_remove_product_fit_product_fit_remove_product_style_and_more.py new file mode 100644 index 00000000..6e7d0bf3 --- /dev/null +++ b/products/migrations/0011_remove_product_fit_product_fit_remove_product_style_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 4.0 on 2024-01-27 07:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0010_alter_product_option_delete_option'), + ] + + operations = [ + migrations.RemoveField( + model_name='product', + name='fit', + ), + migrations.AddField( + model_name='product', + name='fit', + field=models.ManyToManyField(blank=True, null=True, related_name='products', to='products.Fit'), + ), + migrations.RemoveField( + model_name='product', + name='style', + ), + migrations.AddField( + model_name='product', + name='style', + field=models.ManyToManyField(blank=True, null=True, related_name='products', to='products.Style'), + ), + migrations.RemoveField( + model_name='product', + name='texture', + ), + migrations.AddField( + model_name='product', + name='texture', + field=models.ManyToManyField(blank=True, null=True, related_name='products', to='products.Texture'), + ), + ] diff --git a/products/migrations/0012_alter_product_fit_alter_product_style_and_more.py b/products/migrations/0012_alter_product_fit_alter_product_style_and_more.py new file mode 100644 index 00000000..ade05c5b --- /dev/null +++ b/products/migrations/0012_alter_product_fit_alter_product_style_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0 on 2024-01-31 11:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0011_remove_product_fit_product_fit_remove_product_style_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='fit', + field=models.ManyToManyField(blank=True, related_name='products', to='products.Fit'), + ), + migrations.AlterField( + model_name='product', + name='style', + field=models.ManyToManyField(blank=True, related_name='products', to='products.Style'), + ), + migrations.AlterField( + model_name='product', + name='texture', + field=models.ManyToManyField(blank=True, related_name='products', to='products.Texture'), + ), + ] diff --git a/products/migrations/0013_detail_product_detail.py b/products/migrations/0013_detail_product_detail.py new file mode 100644 index 00000000..31d5a819 --- /dev/null +++ b/products/migrations/0013_detail_product_detail.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0 on 2024-01-31 12:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0012_alter_product_fit_alter_product_style_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Detail', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.AddField( + model_name='product', + name='detail', + field=models.ManyToManyField(blank=True, related_name='products', to='products.Detail'), + ), + ] diff --git a/products/models.py b/products/models.py index 6d5f9034..3e8ef8ff 100644 --- a/products/models.py +++ b/products/models.py @@ -2,19 +2,50 @@ from core.models import TimeStampedModel # Create your models here. +class Category(models.Model): + name = models.CharField(max_length=100,blank=False) + +class Style(models.Model): + name = models.CharField(max_length=100,blank=False) + +class Texture(models.Model): + name = models.CharField(max_length=100,blank=False) + +class Fit(models.Model): + name = models.CharField(max_length=100,blank=False) + +class Detail(models.Model): + name = models.CharField(max_length=100,blank=False) + class Product(TimeStampedModel): name = models.CharField(max_length=100) - #category = models.ForeignKey('Category', related_name='products', on_delete=models.CASCADE, null=False, blank=False) + #필터링 항목 + # category = models.ForeignKey('Category', related_name='products', on_delete=models.CASCADE, default=1) + category = models.ForeignKey('Category', related_name='products',on_delete=models.CASCADE, null=True, blank=True) + style = models.ManyToManyField('Style',related_name='products',blank=True) + texture = models.ManyToManyField('Texture',related_name='products',blank=True) + fit = models.ManyToManyField('Fit',related_name='products',blank=True) + detail = models.ManyToManyField('Detail',related_name='products',blank=True) + # option = models.ForeignKey('Option',related_name='products', on_delete=models.CASCADE, null=True, blank = True) + basic_price = models.CharField(max_length=500, blank = False) - option = models.TextField(blank = True) info = models.TextField(blank = True) notice = models.TextField(blank = True) + option = models.TextField(blank = True) period = models.CharField(max_length=100, blank = True) transaction_direct = models.BooleanField(default=False) transaction_package = models.BooleanField(default=False) refund = models.TextField(blank = True) reformer = models.ForeignKey('users.User', related_name='products', on_delete=models.SET_NULL, null=True, blank=False) - + + likeuser_set = models.ManyToManyField("users.User", related_name='liked_products', blank=True) + like_cnt = models.PositiveIntegerField(default=0) + + def like(self): + self.like_cnt+=1 + + def dislike(self): + self.like_cnt-=1 class ProductKeyword(models.Model): diff --git a/products/selectors.py b/products/selectors.py new file mode 100644 index 00000000..5344b347 --- /dev/null +++ b/products/selectors.py @@ -0,0 +1,138 @@ + +## +from datetime import datetime +from dataclasses import dataclass +from django.db.models import Q, Case,Value, When, Exists, OuterRef +from products.models import Product +from users.models import User + + +@dataclass +class ProductDto: + id : int + name : str + basic_price : str + category: dict + reformer : dict + style : list[dict] + texture:list[dict] + fit : list[dict] + detail :list[dict] + + user_likes : bool + + created: datetime + updated: datetime + + like_cnt : int = None + + period : str =None + + option : list[dict] =None + info : str=None + notice : str = None + + keywords: list[str] = None + photos: list[str] = None + + +class ProductSelector: + def __init__(self): + pass + + @staticmethod + def list(search: str,order : str, user: User, + category_filter:str, style_filters:list[str], fit_filters:list[str],texture_filters:list[str],detail_filters:list[str],): + q=Q() + # #검색 조건 후에 수정 + # q.add(Q(info__icontains=search), q.AND) + + if category_filter: + q.add(Q(category__id__iexact=category_filter),q.AND) + + + if style_filters: + style_filter_q = Q() + for style_filter in style_filters: + style_filter_q.add( + Q(style__id=style_filter), q.OR) + q.add(style_filter_q, q.AND) + + if fit_filters: + fit_filter_q=Q() + for fit_filter in fit_filters: + fit_filter_q.add( + Q(fit__id=fit_filter),q.OR) + q.add(fit_filter_q, q.AND) + + if texture_filters: + texture_filter_q=Q() + for texture_filter in texture_filters: + texture_filter_q.add( + Q(texture__id=texture_filter),q.OR) + q.add(texture_filter_q,q.AND) + + if detail_filters: + option_filter_q=Q() + for option_filter in detail_filters: + option_filter_q.add( + Q(option__id=option_filter),q.OR) + q.add(option_filter_q,q.AND) + + order_pair={'latest':'-created', + 'oldest':'created', + 'hot':'-created'} + + products = Product.objects.distinct().annotate( + user_likes = Case( + When(Exists(Product.likeuser_set.through.objects.filter( + product_id=OuterRef('pk'), + user_id=user.pk + )), + then=Value(1)), + default=Value(0), + ), + ).select_related( + 'reformer','category' + ).prefetch_related( + 'keywords','product_photos','style','fit','texture','detail' + ).filter(q).order_by(order_pair[order]) + + products_dtos =[ ProductDto( + id=product.id, + name=product.name, + basic_price=product.basic_price, + reformer=product.reformer, + # reformer={ + # 'nickname':product.reformer.nickname, + # 'profile_image':product.reformer.profile_image, + # }, + user_likes=product.user_likes, + created=product.created.strftime('%Y-%m-%dT%H:%M:%S%z'), + updated=product.updated.strftime('%Y-%m-%dT%H:%M:%S%z'), + + category={'id':product.category.id, + 'name':product.category.name}, + style=[{'id':s.id,'name':s.name} + for s in product.style.all()], + fit=[{'id':f.id,'name':f.name} + for f in product.fit.all()], + texture=[{'id':t.id,'name':t.name} + for t in product.texture.all()], + detail=[{'id':d.id,'name':d.name} + for d in product.detail.all()], + + keywords=[keywords.name for keywords in product.keywords.all()], + photos=[photos.image.url for photos in product.product_photos.all()], + + period=product.period, + # option=product.option, + # info=product.info, + # notice=product.notice, + ) for product in products] + + + return products_dtos + + def likes(self, product:Product, user:User): + return product.likeuser_set.filter(pk=user.pk).exists() \ No newline at end of file diff --git a/products/services.py b/products/services.py index 66ac69d4..a826ed99 100644 --- a/products/services.py +++ b/products/services.py @@ -9,7 +9,8 @@ from django.core.files.uploadedfile import InMemoryUploadedFile from django.conf import settings -from products.models import Product, ProductKeyword, ProductPhoto +from products.models import Product, ProductKeyword, ProductPhoto, Category, Style, Fit, Texture, Detail +from products.selectors import ProductSelector from users.models import User # from .selectors import ProductSelector #from core.exceptions import ApplicationError @@ -22,11 +23,19 @@ def __init__(self, user:User): def create(self, name : str,keywords : list[str],basic_price : str,option : str, product_photos : list[str],info : str,notice : str,period : str,transaction_direct : bool, transaction_package : bool,refund : str, + category : str, style : str, texture : str, fit : str, detail:str, ) -> Product: product_service=ProductService() product= product_service.create( reformer=self.user, + + category=category, + style=style, + texture=texture, + fit=fit, + detail=detail, + name=name, basic_price=basic_price, option=option, @@ -71,10 +80,22 @@ def like_or_dislike(product:Product, user: User)-> bool: @staticmethod def create(name : str,basic_price : str,option : str,info : str,notice : str, - period : str,transaction_direct : bool,transaction_package : bool,refund : str, reformer : User): + period : str,transaction_direct : bool,transaction_package : bool,refund : str, reformer : User, + category : str, style : str, texture : str, fit : str, detail:str,): + category=get_object_or_404(Category,id=category) + style=get_object_or_404(Style,id=style) + texture=get_object_or_404(Texture,id=texture) + fit=get_object_or_404(Fit,id=fit) + detail=get_object_or_404(detail,id=detail) product = Product( name = name, + category = category, + style=style, + texture=texture, + fit=fit, + detail=detail, + basic_price = basic_price, option = option, info = info, @@ -97,11 +118,11 @@ def __init__(self): pass @staticmethod - def create(image:InMemoryUploadedFile, product:Product): + def create(image:InMemoryUploadedFile): ext = image.name.split(".")[-1] file_path = '{}.{}'.format(str(time.time())+str(uuid.uuid4().hex),ext) image = ImageFile(io.BytesIO(image.read()),name=file_path) - product_photo = ProductPhoto(image=image, product=product) + product_photo = ProductPhoto(image=image, product=None) product_photo.full_clean() product_photo.save() diff --git a/products/urls.py b/products/urls.py index c7ec7256..fa439936 100644 --- a/products/urls.py +++ b/products/urls.py @@ -6,5 +6,6 @@ urlpatterns = [ path('create/',ProductCreateApi.as_view(),name='product_create'), path('photos/create/',ProductPhotoCreateApi.as_view(), name='product_photo_create'), - + path('',ProductListApi.as_view(),name='product_list'), + path('/like/',ProductLikeApi.as_view(), name='product_like'), ] diff --git a/products/views.py b/products/views.py index 3a321d3f..b0ecbca1 100644 --- a/products/views.py +++ b/products/views.py @@ -1,14 +1,18 @@ from django.shortcuts import render +from django.shortcuts import get_object_or_404 from rest_framework.views import APIView from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework import serializers, status from rest_framework.response import Response +from rest_framework.pagination import PageNumberPagination +from core.views import get_paginated_response from users.models import User -from .models import Product +from .models import Product, Category, Style, Fit, Texture, Detail from .services import ProductCoordinatorService, ProductPhotoService, ProductKeywordService, ProductService +from .selectors import ProductSelector class ProductCreateApi(APIView): @@ -16,6 +20,13 @@ class ProductCreateApi(APIView): class ProductCreateInputSerializer(serializers.Serializer): name = serializers.CharField() + + category = serializers.CharField() + style = serializers.CharField() + fit = serializers.CharField() + texture = serializers.CharField() + detail=serializers.CharField() + keywords = serializers.ListField(required=False) basic_price = serializers.CharField() option = serializers.CharField() @@ -36,6 +47,13 @@ def post(self,request): product = service.create( name=data.get('name'), + + category = data.get('category'), + style=data.get('style'), + fit=data.get('fit'), + texture=data.get('texture'), + detail=data.get('detail'), + keywords=data.get('keywords', []), basic_price=data.get('basic_price'), option=data.get('option'), @@ -71,11 +89,87 @@ def post(self, request): data = serializers.validated_data product_photo_url = ProductPhotoService.create( - image=data.get('image'), + image=data.get('image') ) return Response({ 'status':'success', 'data':{'location': product_photo_url}, }, status = status.HTTP_201_CREATED) - \ No newline at end of file + + +class ProductListApi(APIView): + permission_classes = (AllowAny,) + + class Pagination(PageNumberPagination): + page_size= 4 + page_size_query_param = 'page_size' + + class ProductListFilterSerializer(serializers.Serializer): + search = serializers.CharField(required=False) + order = serializers.CharField(required=False) + ## 필터 + category_filter = serializers.CharField(required=False) + style_filter = serializers.ListField(required=False) + fit_filter = serializers.ListField(required=False) + texture_filter = serializers.ListField(required=False) + detail_filter = serializers.ListField(required=False) + #금액 & 수선기간 필터링 추가 + + + class ProductListOutputSerializer(serializers.Serializer): + id = serializers.IntegerField() + name = serializers.CharField() + reformer = serializers.CharField() + basic_price = serializers.CharField() + #info = serializers.CharField() + #notice = serializers.CharField() + #period = serializers.CharField() + user_likes = serializers.BooleanField() + category = serializers.DictField() + photos = serializers.ListField() + style = serializers.ListField(child=serializers.DictField()) + texture = serializers.ListField(child=serializers.DictField()) + fit = serializers.ListField(child=serializers.DictField()) + detail = serializers.ListField(child=serializers.DictField()) + + def get(self,request): + filters_serializer = self.ProductListFilterSerializer( + data=request.query_params) + filters_serializer.is_valid(raise_exception=True) + filters = filters_serializer.validated_data + + products = ProductSelector.list( + search=filters.get('search',''), + order=filters.get('order','latest'), + category_filter=filters.get('category_filter',''), + style_filters = filters.get('style_filter',[]), + fit_filters=filters.get('fit_filter',[]), + texture_filters=filters.get('texture_filter',[]), + detail_filters=filters.get('detail_filter',[]), + user=request.user, + ) + + return get_paginated_response( + pagination_class=self.Pagination, + serializer_class=self.ProductListOutputSerializer, + queryset=products, + request=request, + view=self + ) + + + +class ProductLikeApi(APIView): + permission_classes = (IsAuthenticated, ) + + def post(self, request, product_id): + likes = ProductService.like_or_dislike( + product=get_object_or_404(Product,pk=product_id), + user=request.user + ) + + return Response({ + 'status' : 'success', + 'data':{'likes':likes}, + },status=status.HTTP_200_OK) \ No newline at end of file