@@ -47,6 +47,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.postgres',
|
||||
'django_apscheduler',
|
||||
'django_extensions',
|
||||
'django_filters',
|
||||
'rest_framework',
|
||||
'rest_framework_simplejwt',
|
||||
'rest_framework_simplejwt.token_blacklist',
|
||||
@@ -162,6 +163,11 @@ REST_FRAMEWORK = {
|
||||
# 'rest_framework.permissions.IsAdminUser', # 관리자만 접근
|
||||
# 'rest_framework.permissions.AllowAny', # 누구나 접근
|
||||
),
|
||||
|
||||
'DEFAULT_FILTER_BACKENDS': [
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
'rest_framework.filters.OrderingFilter',
|
||||
],
|
||||
}
|
||||
|
||||
REST_USE_JWT = True
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
"""
|
||||
URL configuration for config project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
path('api/user/', include('users.urls')),
|
||||
path('api/code/', include('codes.urls')),
|
||||
path('api/portfolio/', include('portfolios.urls')),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
17
portfolios/filters.py
Normal file
17
portfolios/filters.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django_filters import rest_framework as filters
|
||||
from django.db.models import Q
|
||||
from .models import Portfolio
|
||||
|
||||
class PortfolioFilter(filters.FilterSet):
|
||||
category = filters.CharFilter(method='filter_category')
|
||||
|
||||
def filter_category(self, queryset, name, value):
|
||||
categories = value.split(',')
|
||||
q = Q()
|
||||
for c in categories:
|
||||
q |= Q(category__contains=[c])
|
||||
return queryset.filter(q)
|
||||
|
||||
class Meta:
|
||||
model = Portfolio
|
||||
fields = ['category']
|
||||
@@ -9,13 +9,15 @@ from users.models import User
|
||||
|
||||
|
||||
class Portfolio(BaseModel):
|
||||
name = models.CharField(max_length=20)
|
||||
title = models.CharField(max_length=20)
|
||||
category = ArrayField(models.CharField(max_length=20), default=list)
|
||||
is_published = models.BooleanField(default=False)
|
||||
view_count = models.IntegerField(default=0)
|
||||
like_count = models.IntegerField(default=0)
|
||||
scrab_count = models.IntegerField(default=0)
|
||||
scrap_count = models.IntegerField(default=0)
|
||||
is_represent = models.BooleanField(default=False)
|
||||
thumbnail = models.ImageField(upload_to='', blank=True)
|
||||
code_id = models.CharField(max_length=26, blank=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='portfolios', to_field="id")
|
||||
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='owned_portfolios', to_field="id")
|
||||
likers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='liked_portfolios', blank=True)
|
||||
scrappers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='scrapped_portfolios', blank=True)
|
||||
5
portfolios/paginations.py
Normal file
5
portfolios/paginations.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
class PortfolioPagination(PageNumberPagination):
|
||||
page_size = 12
|
||||
page_query_param = 'page'
|
||||
16
portfolios/serializers.py
Normal file
16
portfolios/serializers.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from .models import *
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
class PortfolioListViewSerializer(serializers.ModelSerializer):
|
||||
nickname = serializers.CharField(source='owner.nickname')
|
||||
profile_image = serializers.ImageField(source='owner.profile_image')
|
||||
|
||||
class Meta:
|
||||
model = Portfolio
|
||||
fields = ['id', 'category', 'thumbnail', 'nickname', 'profile_image', 'view_count', 'like_count', 'scrap_count']
|
||||
|
||||
class PortfolioCreateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Portfolio
|
||||
fields = ['title', 'category', 'owner']
|
||||
40
portfolios/services.py
Normal file
40
portfolios/services.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from .models import Portfolio
|
||||
|
||||
|
||||
ACTION_FIELD_MAP = {
|
||||
'like': 'likers',
|
||||
'scrap': 'scrappers',
|
||||
}
|
||||
|
||||
class PortfolioBeforeRelCheckService:
|
||||
@staticmethod
|
||||
def check_user_portfolio_rel(action_type: str, portfolio: Portfolio, user):
|
||||
action = ACTION_FIELD_MAP.get(action_type)
|
||||
if not action:
|
||||
raise ValueError(f"'{action_type}' is unsupported action type")
|
||||
|
||||
if getattr(portfolio, action).filter(id=user.id).exists():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class PortfolioStateChangeService:
|
||||
@staticmethod
|
||||
def change_user_portfolio_rel(action_type: str, portfolio: Portfolio, user, add=True):
|
||||
action = ACTION_FIELD_MAP.get(action_type)
|
||||
if not action:
|
||||
raise ValueError(f"'{action_type}' is unsupported action type")
|
||||
|
||||
user_method = getattr(getattr(portfolio, action), 'add' if add else 'remove')
|
||||
user_method(user)
|
||||
|
||||
@staticmethod
|
||||
def change_count_portfolio_state(action_type: str, portfolio: Portfolio, add=True):
|
||||
action = ACTION_FIELD_MAP.get(action_type)
|
||||
if not action:
|
||||
raise ValueError(f"'{action_type}' is unsupported action type")
|
||||
field_name = action_type + '_count'
|
||||
current_count = getattr(portfolio, field_name)
|
||||
now_count = current_count+1 if add else max(current_count-1, 0)
|
||||
setattr(portfolio, field_name, now_count)
|
||||
portfolio.save(update_fields=[field_name])
|
||||
15
portfolios/urls.py
Normal file
15
portfolios/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import *
|
||||
|
||||
app_name = 'portfolios'
|
||||
|
||||
urlpatterns = [
|
||||
path('list/', PortfolioListView.as_view()),
|
||||
path('create/', PortfolioCreateAPIView.as_view()),
|
||||
path('<str:pk>/', PortfolioDeleteAPIView.as_view()),
|
||||
path('<str:pk>/set-represent/', PortfolioSetRepresentAPIView.as_view()),
|
||||
path('<str:pk>/set-publish/', PortfolioSetPublishAPIView.as_view()),
|
||||
path('<str:pk>/change-state/', PortfolioChangeState.as_view()),
|
||||
|
||||
]
|
||||
@@ -1,3 +1,105 @@
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
# Create your views here.
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from .models import *
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.generics import ListAPIView
|
||||
from .serializers import PortfolioListViewSerializer, PortfolioCreateSerializer
|
||||
from .paginations import PortfolioPagination
|
||||
from .filters import PortfolioFilter
|
||||
from .services import PortfolioStateChangeService, PortfolioBeforeRelCheckService
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
|
||||
class PortfolioListView(ListAPIView):
|
||||
queryset = Portfolio.objects.filter(is_published=True).order_by('-created_at')
|
||||
serializer_class = PortfolioListViewSerializer
|
||||
pagination_class = PortfolioPagination
|
||||
filterset_class = PortfolioFilter
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter]
|
||||
ordering_fields = ['view_count', 'like_count', 'scrap_count', 'created_at'] # 허용할 필드 추천순 제외
|
||||
ordering = ['-created_at'] # 기본 정렬은 최신순
|
||||
|
||||
class PortfolioCreateAPIView(APIView):
|
||||
@transaction.atomic
|
||||
def post(self, request):
|
||||
data = request.data.copy()
|
||||
data['owner'] = request.user.id
|
||||
serializer = PortfolioCreateSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({"message": "create portfolio success"}, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class PortfolioDeleteAPIView(APIView):
|
||||
@transaction.atomic
|
||||
def delete(self, request, pk):
|
||||
user = request.user
|
||||
portfolio = get_object_or_404(Portfolio, id=pk)
|
||||
if user != portfolio.owner:
|
||||
return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN)
|
||||
portfolio.delete()
|
||||
return Response({"message": "delete success"}, status=status.HTTP_200_OK)
|
||||
|
||||
class PortfolioSetRepresentAPIView(APIView):
|
||||
@transaction.atomic
|
||||
def patch(self, request, pk):
|
||||
user = request.user
|
||||
portfolio = get_object_or_404(Portfolio, id=pk)
|
||||
if user != portfolio.owner:
|
||||
return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN)
|
||||
if before_represent := Portfolio.objects.filter(owner=user, is_represent=True).first():
|
||||
before_represent.is_represent = False
|
||||
portfolio.is_represent = True
|
||||
return Response({"message": "change represent success"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class PortfolioSetPublishAPIView(APIView):
|
||||
@transaction.atomic
|
||||
def patch(self, request, pk):
|
||||
user = request.user
|
||||
portfolio = get_object_or_404(Portfolio, id=pk)
|
||||
if user != portfolio.owner:
|
||||
return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN)
|
||||
portfolio.is_published = True
|
||||
portfolio.save()
|
||||
return Response({"message": "publish success"}, status=status.HTTP_200_OK)
|
||||
|
||||
class PortfolioChangeState(APIView):
|
||||
@transaction.atomic
|
||||
def patch(self, request, pk):
|
||||
portfolio = get_object_or_404(Portfolio, pk=pk)
|
||||
user = request.user
|
||||
action_type = request.query_params.get('type')
|
||||
if PortfolioBeforeRelCheckService.check_user_portfolio_rel(action_type, portfolio, user):
|
||||
return Response({"message": "already done"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return self._handle_action(action_type, portfolio, user, add=True)
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, pk):
|
||||
portfolio = get_object_or_404(Portfolio, pk=pk)
|
||||
user = request.user
|
||||
action_type = request.query_params.get('type')
|
||||
if not PortfolioBeforeRelCheckService.check_user_portfolio_rel(action_type, portfolio, user):
|
||||
return Response({"message": "never done before"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return self._handle_action(action_type, portfolio, user, add=False)
|
||||
|
||||
def _handle_action(self, action_type, portfolio, user, add=True):
|
||||
if not action_type:
|
||||
return Response({'message': 'Missing action type'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
PortfolioStateChangeService.change_user_portfolio_rel(action_type, portfolio, user, add)
|
||||
PortfolioStateChangeService.change_count_portfolio_state(action_type, portfolio, add)
|
||||
except ValueError as e:
|
||||
return Response({'message': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response({
|
||||
'message': f'{action_type} {"added" if add else "removed"}'
|
||||
}, status=status.HTTP_200_OK)
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -21,3 +21,8 @@ class JoinSerializer(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
return User.objects.create_user(**validated_data)
|
||||
|
||||
class SetPortofolioRequiredInfoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['custom_url', 'job_and_interests']
|
||||
@@ -9,4 +9,6 @@ urlpatterns = [
|
||||
path('join/', JoinAPIView.as_view()),
|
||||
path('login/', LoginAPIView.as_view()),
|
||||
path('check/', NicknameAPIView.as_view()),
|
||||
path('portfolio-info/', SetPortofolioRequiredInfoAPIView.as_view()),
|
||||
path('portfolio-info/check/', SetPortofolioRequiredInfoAPIView.as_view())
|
||||
]
|
||||
@@ -95,3 +95,22 @@ class NicknameAPIView(APIView):
|
||||
except:
|
||||
return Response({"message": "사용할 수 있는 닉네임입니다."}, status=status.HTTP_200_OK)
|
||||
|
||||
class SetPortofolioRequiredInfoAPIView(APIView):
|
||||
def get(self, request):
|
||||
custom_url = request.GET.get('custom_url', None)
|
||||
if not custom_url:
|
||||
return Response({"message": "no url"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if User.objects.filter(custom_url=custom_url).exists():
|
||||
return Response({"message": "already used url"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response({"message": "can use this url"}, status=status.HTTP_200_OK)
|
||||
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
serializer = SetPortofolioRequiredInfoSerializer(user, data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
user.is_custom_url = True
|
||||
user.save()
|
||||
return Response({"message": "updated successfully"}, status=status.HTTP_202_ACCEPTED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
Reference in New Issue
Block a user