@@ -47,6 +47,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.postgres',
|
'django.contrib.postgres',
|
||||||
'django_apscheduler',
|
'django_apscheduler',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
|
'django_filters',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework_simplejwt',
|
'rest_framework_simplejwt',
|
||||||
'rest_framework_simplejwt.token_blacklist',
|
'rest_framework_simplejwt.token_blacklist',
|
||||||
@@ -162,6 +163,11 @@ REST_FRAMEWORK = {
|
|||||||
# 'rest_framework.permissions.IsAdminUser', # 관리자만 접근
|
# 'rest_framework.permissions.IsAdminUser', # 관리자만 접근
|
||||||
# 'rest_framework.permissions.AllowAny', # 누구나 접근
|
# 'rest_framework.permissions.AllowAny', # 누구나 접근
|
||||||
),
|
),
|
||||||
|
|
||||||
|
'DEFAULT_FILTER_BACKENDS': [
|
||||||
|
'django_filters.rest_framework.DjangoFilterBackend',
|
||||||
|
'rest_framework.filters.OrderingFilter',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
REST_USE_JWT = True
|
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.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 = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
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):
|
class Portfolio(BaseModel):
|
||||||
name = models.CharField(max_length=20)
|
title = models.CharField(max_length=20)
|
||||||
category = ArrayField(models.CharField(max_length=20), default=list)
|
category = ArrayField(models.CharField(max_length=20), default=list)
|
||||||
is_published = models.BooleanField(default=False)
|
is_published = models.BooleanField(default=False)
|
||||||
view_count = models.IntegerField(default=0)
|
view_count = models.IntegerField(default=0)
|
||||||
like_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)
|
is_represent = models.BooleanField(default=False)
|
||||||
thumbnail = models.ImageField(upload_to='', blank=True)
|
thumbnail = models.ImageField(upload_to='', blank=True)
|
||||||
code_id = models.CharField(max_length=26, 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):
|
def create(self, validated_data):
|
||||||
return User.objects.create_user(**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('join/', JoinAPIView.as_view()),
|
||||||
path('login/', LoginAPIView.as_view()),
|
path('login/', LoginAPIView.as_view()),
|
||||||
path('check/', NicknameAPIView.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:
|
except:
|
||||||
return Response({"message": "사용할 수 있는 닉네임입니다."}, status=status.HTTP_200_OK)
|
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