Merge pull request #42 from plers-org/sm/#41

Sm/#41
This commit is contained in:
NKEY
2025-05-02 18:37:44 +09:00
committed by GitHub
6 changed files with 290 additions and 6 deletions

View File

@@ -1,12 +1,14 @@
from django.db import models from django.db import models
from common.models.baseModels import BaseModel from common.models.baseModels import BaseModel
from common.models.choiceModels import InvitationStatus
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.conf import settings from django.conf import settings
from users.models import User from users.models import User
class Project(BaseModel): class Project(BaseModel):
title = models.CharField(max_length=20) title = models.CharField(max_length=20)
is_team = models.BooleanField(default=False) is_team = models.BooleanField(default=False)
@@ -24,5 +26,12 @@ class Project(BaseModel):
scrappers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='scrapped_projects', blank=True) scrappers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='scrapped_projects', blank=True)
class ProjectTeamList(BaseModel): class ProjectTeamList(BaseModel):
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='project_team_list', to_field='id') project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='team_project_member_list', to_field='id')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='project_team_list',to_field='id') user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='team_project_list',to_field='id')
class ProjectInvitation(BaseModel):
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_invitations')
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_invitations')
project = models.ForeignKey(Project, on_delete=models.CASCADE)
status = models.CharField(max_length=10, choices=InvitationStatus.choices)

View File

@@ -6,6 +6,8 @@ from common.utils.codeManger import set_expire
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from common.models.choiceModels import NotificationType
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
def create_user(self, email, password, **kwargs): def create_user(self, email, password, **kwargs):
user = self.model(email = email, **kwargs) user = self.model(email = email, **kwargs)
@@ -47,6 +49,7 @@ class User(BaseModel, AbstractBaseUser, PermissionsMixin):
external_links = ArrayField(models.TextField(), default=list, blank=True) external_links = ArrayField(models.TextField(), default=list, blank=True)
short_bio = models.CharField(max_length=100, blank=True) short_bio = models.CharField(max_length=100, blank=True)
profile_image = models.ImageField(upload_to='', blank=True) profile_image = models.ImageField(upload_to='', blank=True)
banner_image = models.ImageField(upload_to='', blank=True)
is_staff = models.BooleanField(default=False) is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
@@ -58,4 +61,10 @@ class User(BaseModel, AbstractBaseUser, PermissionsMixin):
def __str__(self): def __str__(self):
return self.nickname return self.nickname
class Notification(BaseModel):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
content = models.TextField()
is_read = models.BooleanField(default=False)
note_type = models.CharField(max_length=10, choices=NotificationType.choices)

View File

@@ -30,4 +30,40 @@ class SetPortofolioRequiredInfoSerializer(serializers.ModelSerializer):
class TagUserSerializer(serializers.ModelSerializer): class TagUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['nickname', 'profile_image'] fields = ['nickname', 'profile_image']
class UserProfileSerializer(serializers.ModelSerializer):
represent_portfolio_id = serializers.CharField(read_only=True)
new_notification_count = serializers.IntegerField(read_only=True)
class Meta:
model = User
fields = [
'banner_image',
'profile_image',
'nickname',
'external_links',
'job_and_interests',
'skills',
'short_bio',
'represent_portfolio_id',
'new_notification_count'
]
class UserMemberInfoSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
'realname',
'email',
'phone',
'nickname',
'gender',
'birth_date',
'custom_url',
'job_and_interests'
]
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model = Notification
fields = ['id', 'content', 'note_type', 'is_read']

85
users/services.py Normal file
View File

@@ -0,0 +1,85 @@
from .models import *
from projects.models import *
from portfolios.models import *
from django.utils import timezone
from datetime import timedelta
# 30일 이전 일수 계산
thirty_days_ago = timezone.now() - timedelta(days=30)
class CheckUserFieldDuplicateService:
@staticmethod
def check_nickname_duplicate(query):
pass
@staticmethod
def check_custom_url_duplicate():
pass
# 알림 관련 서비스 로직
class NotifiationService:
@staticmethod
def set_content_by_type():
pass
class UserToNotificationService:
@staticmethod
def get_new_notification_count(user: User):
return user.notifications.filter(created_at__lt=thirty_days_ago, is_read=False).count()
@staticmethod
def get_all_notification(user: User):
return user.notifications.filter(created_at__lt=thirty_days_ago)
# 유저 -> 포트폴리오 관련 서비스 로직
class UserToPortfolioService:
@staticmethod
def get_represent_portfolio(user: User):
if represent_portfolio := user.owned_portfolios.filter(is_represent=True).first():
return represent_portfolio.id
else:
return None
@staticmethod
def get_published_portfolio(user: User):
return user.owned_portfolios.filter(is_published=True)
@staticmethod
def get_unpublished_portfolio(user: User):
return user.owned_portfolios.filter(is_published=False)
@staticmethod
def get_scrap_portfolio(user: User):
return user.scrapped_portfolios.filter(is_published=True)
# 유저 -> 프로젝트 관련 서비스 로직
class UserToProjectService:
@staticmethod
def get_published_solo_project(user: User):
return user.owned_projects.filter(is_team=False, is_published=True)
@staticmethod
def get_unpublished_solo_project(user: User):
return user.owned_projects.filter(is_team=False, is_published=False)
@staticmethod
def get_published_team_project(user: User):
return Project.objects.filter(
team_project_member_list__user = user,
is_published=True
).distinct()
@staticmethod
def get_unpublished_team_project(user: User):
return Project.objects.filter(
team_project_member_list__user = user,
is_published=False
).distinct()
@staticmethod
def get_scrap_project(user: User):
return user.scrapped_projects.filter(is_published=True)

View File

@@ -1,9 +1,14 @@
from django.urls import path from django.urls import path, include
from .views import * from .views import *
from rest_framework.routers import DefaultRouter
app_name = 'users' app_name = 'users'
router = DefaultRouter()
router.register(r'notifications', NotificationReadViewSet, basename='notification')
urlpatterns = [ urlpatterns = [
path('refresh-token/', RefreshAPIView.as_view()), path('refresh-token/', RefreshAPIView.as_view()),
path('join/', JoinAPIView.as_view()), path('join/', JoinAPIView.as_view()),
@@ -12,4 +17,8 @@ urlpatterns = [
path('portfolio-info/', SetPortofolioRequiredInfoAPIView.as_view()), path('portfolio-info/', SetPortofolioRequiredInfoAPIView.as_view()),
path('portfolio-info/check/', SetPortofolioRequiredInfoAPIView.as_view()), path('portfolio-info/check/', SetPortofolioRequiredInfoAPIView.as_view()),
path('teamtag/', TagUserAPIView.as_view()), path('teamtag/', TagUserAPIView.as_view()),
path('mypage/profile/<str:nickname>/', MyPageProfileAPIView.as_view()),
path('mypage/works/<str:nickname>/', MyPageWorkListAPIView.as_view()),
path('mypage/my-info/', MyPageMemberInfoAPIView.as_view()),
path('', include(router.urls)),
] ]

View File

@@ -3,6 +3,7 @@ from django.conf import settings
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, TokenRefreshSerializer from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, TokenRefreshSerializer
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
@@ -16,11 +17,18 @@ from django.db.models import Case, When, Value, IntegerField, Q
from .models import * from .models import *
from .serializers import * from .serializers import *
from .services import *
from projects.serializers import *
from portfolios.serializers import *
from django.db import transaction
class RefreshAPIView(APIView): class RefreshAPIView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
# access token 재발급 # access token 재발급
@transaction.atomic
def post(self, request): def post(self, request):
refresh = request.COOKIES.get("refresh") refresh = request.COOKIES.get("refresh")
if not refresh: if not refresh:
@@ -41,6 +49,7 @@ class RefreshAPIView(APIView):
class JoinAPIView(APIView): class JoinAPIView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
# 회원가입 # 회원가입
@transaction.atomic
def post(self, request): def post(self, request):
serializer = JoinSerializer(data=request.data) serializer = JoinSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
@@ -52,6 +61,7 @@ class JoinAPIView(APIView):
class LoginAPIView(APIView): class LoginAPIView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
# 로그인 # 로그인
@transaction.atomic
def post(self, request): def post(self, request):
email=request.data.get("email", None) email=request.data.get("email", None)
password=request.data.get("password", None) password=request.data.get("password", None)
@@ -122,6 +132,7 @@ class SetPortofolioRequiredInfoAPIView(APIView):
else: else:
return Response({"message": "can use this url"}, status=status.HTTP_200_OK) return Response({"message": "can use this url"}, status=status.HTTP_200_OK)
@transaction.atomic
def patch(self, request): def patch(self, request):
user = request.user user = request.user
serializer = SetPortofolioRequiredInfoSerializer(user, data=request.data) serializer = SetPortofolioRequiredInfoSerializer(user, data=request.data)
@@ -130,4 +141,129 @@ class SetPortofolioRequiredInfoAPIView(APIView):
user.is_custom_url = True user.is_custom_url = True
user.save() user.save()
return Response({"message": "updated successfully"}, status=status.HTTP_202_ACCEPTED) return Response({"message": "updated successfully"}, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# 마이페이지 관련
class MyPageProfileAPIView(APIView):
# 프로필 조회(안 읽은 알림 수)
def get(self, request, nickname):
target_user = get_object_or_404(User, nickname=nickname)
user = request.user
serializer = UserProfileSerializer(target_user)
data = serializer.data
data['represent_portfolio_id'] = UserToPortfolioService.get_represent_portfolio(target_user)
data['new_notification_count'] = UserToNotificationService.get_new_notification_count(user)
return Response(data, status=status.HTTP_200_OK)
# 프로필 수정
@transaction.atomic
def patch(self, request, nickname):
target_user = get_object_or_404(User, nickname=nickname)
user = request.user
if user == target_user:
serializer = UserProfileSerializer(user, request.data, partial=True)
if serializer.is_valid():
serializer.save()
data = serializer.data
data['represent_portfolio_id'] = UserToPortfolioService.get_represent_portfolio(target_user)
data['new_notification_count'] = UserToNotificationService.get_new_notification_count(user)
return Response(data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({"message": "Not account owner"}, status=status.HTTP_400_BAD_REQUEST)
class MyPageWorkListAPIView(APIView):
# 포폴, 플젝, 작업중, 스크랩 조회(안 읽은 알림 수)
def get(self, request, nickname):
target_user = get_object_or_404(User, nickname=nickname)
user = request.user
retreive_type = request.query_params.get('type')
data = {}
if retreive_type == 'portfolio':
portfolios = UserToPortfolioService.get_published_portfolio(target_user)
serializer = PortfolioListViewSerializer(portfolios, many=True)
data = {
"portfolios": serializer.data
}
elif retreive_type == 'project':
solo_projects = UserToProjectService.get_published_solo_project(target_user)
team_projects = UserToProjectService.get_published_team_project(target_user)
sp_serializer = ProjectListViewSerializer(solo_projects, many=True)
tp_serializer = ProjectListViewSerializer(team_projects, many=True)
data = {
"solo_projects": sp_serializer.data,
"team_projects": tp_serializer.data,
}
elif retreive_type == 'working':
if user != target_user:
return Response({"message": "Not account owner"}, status=status.HTTP_400_BAD_REQUEST)
portfolios = UserToPortfolioService.get_unpublished_portfolio(target_user)
solo_projects = UserToProjectService.get_unpublished_solo_project(target_user)
team_projects = UserToProjectService.get_unpublished_team_project(target_user)
po_serializer = PortfolioListViewSerializer(portfolios, many=True)
sp_serializer = ProjectListViewSerializer(solo_projects, many=True)
tp_serializer = ProjectListViewSerializer(team_projects, many=True)
data = {
"portfolios": po_serializer.data,
"solo_projects": sp_serializer.data,
"team_projects": tp_serializer.data,
}
elif retreive_type == 'scrap':
if user != target_user:
return Response({"message": "Not account owner"}, status=status.HTTP_400_BAD_REQUEST)
portfolios = UserToPortfolioService.get_scrap_portfolio(target_user)
po_serializer = PortfolioListViewSerializer(portfolios, many=True)
projects = UserToProjectService.get_scrap_project(target_user)
pr_serializer = ProjectListViewSerializer(projects, many=True)
data = {
"portfolios": po_serializer.data,
"projects": pr_serializer.data,
}
else:
return Response({"message": "not allowed retreive_type"}, status=status.HTTP_400_BAD_REQUEST)
data['new_notification_count'] = UserToNotificationService.get_new_notification_count(user)
return Response(data, status=status.HTTP_200_OK)
class MyPageMemberInfoAPIView(APIView):
# 내 정보 조회(안 읽은 알림 수)
def get(self, request):
user = request.user
serializer = UserMemberInfoSerializer(user)
data = serializer.data
data['new_notification_count'] = UserToNotificationService.get_new_notification_count(user)
return Response(data, status=status.HTTP_200_OK)
# 내 정보 수정
@transaction.atomic
def patch(self, request):
user = request.user
serializer = UserMemberInfoSerializer(user, request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class NotificationReadViewSet(ReadOnlyModelViewSet):
serializer_class = NotificationSerializer
# 30일 이전 알림만 가져옴
def get_queryset(self):
return UserToNotificationService.get_all_notification(self.request.user)
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
instance.is_read = True
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data, status=status.HTTP_200_OK)