Merge pull request #23 from plers-org/Feat/#22

Feat/#22
This commit is contained in:
NKEY
2025-04-12 02:08:26 +09:00
committed by GitHub
13 changed files with 247 additions and 25 deletions

View File

@@ -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

View File

@@ -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
View 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']

View File

@@ -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)

View 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
View 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
View 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
View 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()),
]

View File

@@ -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)

Binary file not shown.

View File

@@ -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']

View File

@@ -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())
] ]

View File

@@ -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)