From b6a14b11d085b127ccc0497bf2832bd8026b64ab Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 2 May 2025 18:13:49 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20[#41]=20projectteamli?= =?UTF-8?q?st=20=EC=97=AD=EC=B0=B8=EC=A1=B0=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- projects/models.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/projects/models.py b/projects/models.py index 8d40c5b..88ed65d 100644 --- a/projects/models.py +++ b/projects/models.py @@ -1,12 +1,14 @@ from django.db import models from common.models.baseModels import BaseModel +from common.models.choiceModels import InvitationStatus from django.contrib.postgres.fields import ArrayField from django.conf import settings from users.models import User + class Project(BaseModel): title = models.CharField(max_length=20) 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) class ProjectTeamList(BaseModel): - project = models.ForeignKey(Project, 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='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='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) From 3ca5bc2f9c56db70a4871a195061cc0ec0f9a6b2 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 2 May 2025 18:14:59 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#41]=20banner=5Fimage?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20notifi?= =?UTF-8?q?cation=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/models.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/users/models.py b/users/models.py index 2c75099..ac5c4ab 100644 --- a/users/models.py +++ b/users/models.py @@ -6,6 +6,8 @@ from common.utils.codeManger import set_expire from django.contrib.postgres.fields import ArrayField from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from common.models.choiceModels import NotificationType + class UserManager(BaseUserManager): def create_user(self, email, password, **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) short_bio = models.CharField(max_length=100, 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_active = models.BooleanField(default=True) @@ -58,4 +61,10 @@ class User(BaseModel, AbstractBaseUser, PermissionsMixin): def __str__(self): return self.nickname - \ No newline at end of file + + +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) From 8fd42d5e74b886386a1e2851cfc4841fb16e4a92 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 2 May 2025 18:16:02 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#41]=20user=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84,=20=ED=9A=8C=EC=9B=90=EC=A0=95?= =?UTF-8?q?=EB=B3=B4,=20=EC=95=8C=EB=A6=BC=20=EC=8B=9C=EB=A6=AC=EC=96=BC?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=A0=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/serializers.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/users/serializers.py b/users/serializers.py index 6fe0281..05f9dc5 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -30,4 +30,40 @@ class SetPortofolioRequiredInfoSerializer(serializers.ModelSerializer): class TagUserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['nickname', 'profile_image'] \ No newline at end of file + 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'] \ No newline at end of file From fecca366c890cd8a9b63c2c92474c08c7595c69b Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 2 May 2025 18:17:05 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#41]=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=99=80=20=EB=8B=A4=EB=A5=B8=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=EA=B0=84=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/services.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 users/services.py diff --git a/users/services.py b/users/services.py new file mode 100644 index 0000000..9ea3ba6 --- /dev/null +++ b/users/services.py @@ -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) + From ca63f4798fe683d5233aa062ffd2469e9278f355 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 2 May 2025 18:24:15 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#41]=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20url=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/urls.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/users/urls.py b/users/urls.py index 5cb23f5..a73f9a1 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,9 +1,14 @@ -from django.urls import path +from django.urls import path, include from .views import * +from rest_framework.routers import DefaultRouter + app_name = 'users' +router = DefaultRouter() +router.register(r'notifications', NotificationReadViewSet, basename='notification') + urlpatterns = [ path('refresh-token/', RefreshAPIView.as_view()), path('join/', JoinAPIView.as_view()), @@ -12,4 +17,8 @@ urlpatterns = [ path('portfolio-info/', SetPortofolioRequiredInfoAPIView.as_view()), path('portfolio-info/check/', SetPortofolioRequiredInfoAPIView.as_view()), path('teamtag/', TagUserAPIView.as_view()), + path('mypage/profile//', MyPageProfileAPIView.as_view()), + path('mypage/works//', MyPageWorkListAPIView.as_view()), + path('mypage/my-info/', MyPageMemberInfoAPIView.as_view()), + path('', include(router.urls)), ] \ No newline at end of file From 9512b200de1ddb28abfec3005971085aed2ff7b8 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 2 May 2025 18:35:04 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#41]=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/views.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/users/views.py b/users/views.py index c382a2e..5713225 100644 --- a/users/views.py +++ b/users/views.py @@ -3,6 +3,7 @@ from django.conf import settings from django.shortcuts import get_object_or_404 from rest_framework.views import APIView +from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, TokenRefreshSerializer from rest_framework_simplejwt.tokens import RefreshToken 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 .serializers import * +from .services import * + +from projects.serializers import * +from portfolios.serializers import * + +from django.db import transaction class RefreshAPIView(APIView): permission_classes = [AllowAny] # access token 재발급 + @transaction.atomic def post(self, request): refresh = request.COOKIES.get("refresh") if not refresh: @@ -41,6 +49,7 @@ class RefreshAPIView(APIView): class JoinAPIView(APIView): permission_classes = [AllowAny] # 회원가입 + @transaction.atomic def post(self, request): serializer = JoinSerializer(data=request.data) if serializer.is_valid(): @@ -52,6 +61,7 @@ class JoinAPIView(APIView): class LoginAPIView(APIView): permission_classes = [AllowAny] # 로그인 + @transaction.atomic def post(self, request): email=request.data.get("email", None) password=request.data.get("password", None) @@ -122,6 +132,7 @@ class SetPortofolioRequiredInfoAPIView(APIView): else: return Response({"message": "can use this url"}, status=status.HTTP_200_OK) + @transaction.atomic def patch(self, request): user = request.user serializer = SetPortofolioRequiredInfoSerializer(user, data=request.data) @@ -130,4 +141,129 @@ class SetPortofolioRequiredInfoAPIView(APIView): 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) \ No newline at end of file + 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) \ No newline at end of file