Skip to content

Commit 4109984

Browse files
authored
Merge: 내 상품 목록 조회, 상품에 스타일 태그 추가
[Feature/mypage] 내 상품 목록 조회, 상품에 스타일 태그 추가
2 parents bc97576 + d0aa3ee commit 4109984

21 files changed

+211
-19
lines changed

apps/category/admin.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import django_stubs_ext
22
from django.contrib import admin
33

4-
from .models import Category
4+
from .models import Category, Style
55

66
django_stubs_ext.monkeypatch()
77

@@ -10,3 +10,9 @@
1010
class CategoryAdmin(admin.ModelAdmin[Category]):
1111
list_display = ("id", "name", "updated_at", "created_at")
1212
search_fields = ("name",)
13+
14+
15+
@admin.register(Style)
16+
class StyleAdmin(admin.ModelAdmin[Style]):
17+
list_display = ("id", "name", "updated_at", "created_at")
18+
search_fields = ("name",)

apps/category/models.py

+7
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ class Meta:
1212

1313
def __str__(self) -> str:
1414
return self.name
15+
16+
17+
class Style(BaseModel):
18+
name = models.CharField(max_length=20, unique=True)
19+
20+
def __str__(self) -> str:
21+
return self.name

apps/category/serializers.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from rest_framework import serializers
22

3-
from apps.category.models import Category
3+
from apps.category.models import Category, Style
44

55

66
class CategorySerializer(serializers.ModelSerializer[Category]):
77
class Meta:
88
model = Category
99
fields = ("id", "name")
10+
11+
12+
class StyleSerializer(serializers.ModelSerializer[Style]):
13+
class Meta:
14+
model = Style
15+
fields = ("id", "name")

apps/category/tests.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from rest_framework import status
44
from rest_framework.test import APIClient
55

6-
from apps.category.models import Category
6+
from apps.category.models import Category, Style
77

88

9-
class CategoryModelTest(TestCase):
9+
class CategoryListTest(TestCase):
1010
def setUp(self) -> None:
1111
Category.objects.create(name="test_category1")
1212
Category.objects.create(name="test_category2")
@@ -21,3 +21,16 @@ def test_get_all_categories(self) -> None:
2121
self.assertEqual(response.status_code, status.HTTP_200_OK)
2222
# self.assertEqual(response.data.get("count"), 2)
2323
self.assertEqual(len(response.data), 2)
24+
25+
26+
class StyleListTest(TestCase):
27+
def setUp(self) -> None:
28+
Style.objects.create(name="test_style1")
29+
Style.objects.create(name="test_style2")
30+
31+
def test_get_all_styles(self) -> None:
32+
client = APIClient()
33+
url = reverse("style-list")
34+
response = client.get(url)
35+
self.assertEqual(response.status_code, status.HTTP_200_OK)
36+
self.assertEqual(len(response.data), 2)

apps/category/urls.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from django.urls import path
22

3-
from apps.category.views import CategoryListView
3+
from apps.category.views import CategoryListView, StyleListView
44

55
urlpatterns = [
66
path("", CategoryListView.as_view(), name="category-list"),
7+
path("styles/", StyleListView.as_view(), name="style-list"),
78
]

apps/category/views.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
from rest_framework import generics, permissions
22

3-
from apps.category.models import Category
4-
from apps.category.serializers import CategorySerializer
3+
from apps.category.models import Category, Style
4+
from apps.category.serializers import CategorySerializer, StyleSerializer
55

66

77
class CategoryListView(generics.ListAPIView[Category]):
88
queryset = Category.objects.all()
99
serializer_class = CategorySerializer
1010
permission_classes = [permissions.AllowAny]
1111
pagination_class = None
12+
13+
14+
class StyleListView(generics.ListAPIView[Style]):
15+
queryset = Style.objects.all()
16+
serializer_class = StyleSerializer
17+
permission_classes = [permissions.AllowAny]
18+
pagination_class = None

apps/mypage/__init__.py

Whitespace-only changes.

apps/mypage/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class MypageConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "apps.mypage"

apps/mypage/models.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# from django.db import models
2+
#
3+
# from apps.category.models import Style
4+
# from apps.common.models import BaseModel
5+
# from apps.user.models import Account
6+
#
7+
#
8+
# class InterestedStyle(BaseModel):
9+
# user = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='users')
10+
# styles = models.ManyToManyField(Style, related_name="styles")
11+
# # styles = models.ForeignKey(Style, on_delete=models.CASCADE, related_name="styles")

apps/mypage/serializers.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from rest_framework import serializers
2+
3+
from apps.category.models import Style
4+
from apps.category.serializers import StyleSerializer
5+
from apps.product.models import Product
6+
from apps.product.serializers import ProductImageSerializer
7+
8+
9+
class MyProductSerializer(serializers.ModelSerializer[Product]):
10+
images = ProductImageSerializer(many=True, read_only=True)
11+
12+
class Meta:
13+
model = Product
14+
fields = (
15+
"url",
16+
"uuid",
17+
"name",
18+
"brand",
19+
"condition",
20+
"description",
21+
"purchase_date",
22+
"purchase_price",
23+
"rental_fee",
24+
"size",
25+
"views",
26+
"product_category",
27+
"styles",
28+
"status",
29+
"amount",
30+
"region",
31+
"created_at",
32+
"updated_at",
33+
"images",
34+
"likes",
35+
)

apps/mypage/tests.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from django.test import TestCase
2+
from django.urls import reverse
3+
from rest_framework import status
4+
from rest_framework.test import APIClient, APITestCase
5+
6+
from apps.category.models import Category
7+
from apps.product.models import Product
8+
from apps.user.models import Account
9+
10+
11+
class MyProductTest(APITestCase):
12+
def setUp(self) -> None:
13+
self.client = APIClient()
14+
self.url = reverse("my-products")
15+
data = {
16+
"email": "[email protected]",
17+
"password": "fels3570",
18+
"nickname": "nick",
19+
"phone": "1234",
20+
}
21+
self.user = Account.objects.create_user(**data)
22+
self.category = Category.objects.create(name="test category")
23+
self.product = Product.objects.create(
24+
name="test product",
25+
lender=self.user,
26+
brand="test brand",
27+
condition="good",
28+
purchase_date="2024-01-01",
29+
purchase_price=10000,
30+
rental_fee=1000,
31+
size="xs",
32+
product_category=self.category,
33+
)
34+
35+
def test_get_my_products(self) -> None:
36+
self.client.force_authenticate(user=self.user)
37+
res = self.client.get(self.url)
38+
self.assertEqual(res.status_code, status.HTTP_200_OK)

apps/mypage/urls.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.urls import path
2+
3+
from apps.mypage import views
4+
5+
urlpatterns = [
6+
path("products/", views.MyProductListView.as_view(), name="my-products"),
7+
]

apps/mypage/views.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django.db.models import QuerySet
2+
from django.shortcuts import render
3+
from rest_framework import generics, permissions
4+
from rest_framework.exceptions import PermissionDenied
5+
6+
from apps.mypage.serializers import MyProductSerializer
7+
from apps.product.models import Product
8+
from apps.product.serializers import ProductSerializer
9+
from apps.user.models import Account
10+
11+
12+
class MyProductListView(generics.ListAPIView[Product]):
13+
serializer_class = MyProductSerializer
14+
permission_classes = [permissions.IsAuthenticated]
15+
16+
def get_queryset(self) -> QuerySet[Product]:
17+
lender = self.request.user
18+
if not isinstance(lender, Account):
19+
raise PermissionDenied("You must be logged in to view your likes.")
20+
return Product.objects.filter(lender=lender).order_by("-created_at")

apps/product/models.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.db import models
55

6+
from apps.category.models import Style
67
from apps.common.models import BaseModel
78
from apps.common.utils import uuid4_generator
89
from apps.user.models import Account
@@ -22,7 +23,7 @@ class Product(BaseModel):
2223
size = models.CharField(max_length=10) # 사이즈
2324
views = models.IntegerField(default=0) # 조회수
2425
product_category = models.ForeignKey("category.Category", on_delete=models.CASCADE, related_name="products")
25-
# style_category = models.ManyToManyField(StyleCategory)
26+
styles = models.ManyToManyField(Style, blank=True, related_name="products")
2627
status = models.BooleanField(default=True) # 대여 가능 여부
2728
amount = models.IntegerField(default=1)
2829
region = models.CharField(max_length=30, default="None")

apps/product/serializers.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from rest_framework.fields import ReadOnlyField
1010
from rest_framework.utils.serializer_helpers import ReturnDict
1111

12+
from apps.category.models import Style
13+
from apps.like.models import Like
1214
from apps.product.models import Product, ProductImage, RentalHistory
1315
from apps.user.serializers import UserInfoSerializer
1416

@@ -35,6 +37,7 @@ class ProductSerializer(serializers.ModelSerializer[Product]):
3537
# rental_history = RentalHistorySerializer(many=True, read_only=True)
3638
# images = serializers.SerializerMethodField()
3739
images = ProductImageSerializer(many=True, read_only=True)
40+
is_liked = serializers.SerializerMethodField()
3841

3942
class Meta:
4043
model = Product
@@ -52,21 +55,42 @@ class Meta:
5255
"size",
5356
"views",
5457
"product_category",
58+
"styles",
5559
"status",
5660
"amount",
5761
"region",
5862
"created_at",
5963
"updated_at",
6064
"images",
6165
"likes",
66+
"is_liked",
6267
# "rental_history",
6368
)
64-
read_only_fields = ("created_at", "updated_at", "views", "lender", "status", "likes")
69+
read_only_fields = ("created_at", "updated_at", "views", "lender", "status", "likes", "is_liked")
70+
71+
def get_is_liked(self, obj: Product) -> bool:
72+
user = self.context["request"].user
73+
if user.is_authenticated:
74+
return Like.objects.filter(user=user, product=obj).exists()
75+
return False
76+
77+
def set_styles(self, styles_data: list[Style]) -> list[Style]:
78+
styles = []
79+
for style in styles_data:
80+
style_name = style.name
81+
style_item, _ = Style.objects.get_or_create(name=style_name)
82+
styles.append(style)
83+
return styles
6584

6685
@transaction.atomic
6786
def create(self, validated_data: Any) -> Product:
6887
image_set = self.context["request"].FILES.getlist("image")
88+
styles_data = validated_data.pop("styles", [])
6989
product = Product.objects.create(**validated_data)
90+
91+
styles = self.set_styles(styles_data)
92+
product.styles.set(styles)
93+
7094
if image_set:
7195
product_images = [ProductImage(product=product, image=image) for image in image_set]
7296
ProductImage.objects.bulk_create(product_images)
@@ -77,6 +101,7 @@ def update(self, instance: Product, validated_data: Any) -> Product:
77101
request = self.context["request"]
78102
received_new_images = request.FILES.getlist("image")
79103
received_existing_images = request.POST.getlist("image")
104+
styles_data = validated_data.pop("styles", [])
80105

81106
# 기존 이미지와 받은 이미지 id 비교해서 다시 안 온 이미지 삭제
82107
existing_images = {img.get_image_url(): img.id for img in instance.images.all()}
@@ -95,4 +120,8 @@ def update(self, instance: Product, validated_data: Any) -> Product:
95120
for attr, value in validated_data.items():
96121
setattr(instance, attr, value)
97122
instance.save()
123+
124+
# styles 태그 등록
125+
styles = self.set_styles(styles_data)
126+
instance.styles.set(styles)
98127
return instance

apps/product/views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ class ProductViewSet(viewsets.ModelViewSet[Product]):
2626
serializer_class = ProductSerializer
2727
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsLenderOrReadOnly]
2828
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
29-
filterset_fields = ["status", "product_category", "condition", "size"]
29+
filterset_fields = ["status", "product_category", "condition", "size", "styles"]
3030
search_fields = ["name", "lender__nickname"]
31-
ordering_fields = ["created_at", "rental_fee", "views"]
31+
ordering_fields = ["created_at", "rental_fee", "views", "likes"]
3232
parser_classes = [MultiPartParser, FormParser]
3333

3434
def perform_create(self, serializer: BaseSerializer[Product]) -> None:

apps/user/models.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.contrib.auth.models import AbstractUser, PermissionsMixin
55
from django.db import models
66

7-
from apps.category.models import Category
7+
from apps.category.models import Category, Style
88
from apps.common.models import BaseModel
99
from apps.common.utils import uuid4_generator
1010

@@ -44,7 +44,7 @@ class Account(AbstractBaseUser, PermissionsMixin, BaseModel):
4444
region = models.CharField(max_length=30, null=True, blank=True)
4545
phone = models.CharField(max_length=15)
4646
grade = models.CharField(max_length=10, null=True, blank=True)
47-
# interest_cate = models.ManyToManyField(Category, blank=True)
47+
# styles = models.ManyToManyField(Style, blank=True)
4848
profile_img = models.ImageField(upload_to=upload_to_s3_account, null=True, blank=True)
4949
is_staff = models.BooleanField(default=False)
5050
is_active = models.BooleanField(default=True)

apps/user/serializers.py

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from django.db.models import Model
77
from rest_framework import exceptions, serializers
88

9+
from apps.category.models import Style
10+
from apps.category.serializers import StyleSerializer
911
from apps.user.models import Account
1012

1113

@@ -34,6 +36,7 @@ class UserInfoSerializer(UserDetailsSerializer): # type: ignore
3436
region = serializers.CharField(required=False, allow_blank=True)
3537
grade = serializers.CharField(required=False, allow_blank=True)
3638
profile_img = serializers.ImageField(required=False, use_url=True, allow_empty_file=True, allow_null=True)
39+
# styles = StyleSerializer(many=True, required=False, allow_null=True)
3740

3841
class Meta:
3942
model = Account
@@ -49,6 +52,7 @@ class Meta:
4952
"region",
5053
"grade",
5154
"profile_img",
55+
# "styles",
5256
)
5357

5458
def validate_nickname(self, nickname: str) -> str:

config/settings/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"apps.chat",
6969
"apps.notification",
7070
"apps.like",
71+
"apps.mypage",
7172
]
7273

7374
INSTALLED_APPS = DJANGO_SYSTEM_APPS + CUSTOM_USER_APPS

0 commit comments

Comments
 (0)