diff --git a/codes/models.py b/codes/models.py index 43c84c7..21ba956 100644 --- a/codes/models.py +++ b/codes/models.py @@ -4,6 +4,11 @@ from common.models.baseModels import BaseModel from common.models.choiceModels import CertificateCodeUseType, InviteCodeUseType from common.utils.codeManger import set_expire +# from django.contrib.contenttypes.fields import GenericForeignKey +# from django.contrib.contenttypes.models import ContentType + +# from projects.models import Project + class CertificationCode(BaseModel): use_type = models.CharField(choices=CertificateCodeUseType.choices, max_length=5) @@ -17,3 +22,4 @@ class InviteCode(BaseModel): code = models.CharField(max_length=10) expire_at = models.DateTimeField(default=set_expire) # 일주일은 10080분 identifier = models.CharField(max_length=40) + diff --git a/codes/serializers.py b/codes/serializers.py index ea32359..2b6b112 100644 --- a/codes/serializers.py +++ b/codes/serializers.py @@ -8,3 +8,7 @@ from common.utils.codeManger import set_expire, generate_code class CertificateCodeSerializer(serializers.Serializer): identifier = serializers.CharField(max_length=40, write_only=True) code = serializers.CharField(max_length=6, write_only=True, required=False) + +class InviteCodeSerializer(serializers.Serializer): + identifier = serializers.CharField(max_length=40, write_only=True) + code = serializers.CharField(max_length=10, write_only=True, required=False) \ No newline at end of file diff --git a/codes/services.py b/codes/services.py index 0ffe8ad..7b6693c 100644 --- a/codes/services.py +++ b/codes/services.py @@ -10,16 +10,25 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status -from .models import CertificationCode +from .models import * -from common.models.choiceModels import CertificateCodeUseType +from common.models.choiceModels import CertificateCodeUseType, InviteCodeUseType from common.utils.codeManger import set_expire from solapi import SolapiMessageService from solapi.model import RequestMessage +from users.models import User + +from projects.models import Project, ProjectTeamList + # from .schemas import send_sms_post_schema # Swagger나 drf-spectacular 등에 사용되는 데코레이터 +INVITE_CHOICE_USE_TYPE ={ + 'p': InviteCodeUseType.PROJECT, + 'h': InviteCodeUseType.HACKATHON +} + class CertificateService: @staticmethod def send(code, identifier): @@ -79,4 +88,46 @@ class SmsService(CertificateService): return True except Exception as e: # print(f"메시지 발송 실패: {str(e)}") - return False \ No newline at end of file + return False + + +class InviteService: + + # 종류마다 사용하는 테이블이 다르므로 오버라이딩 + @staticmethod + def add_member(invitee, work): + pass + + # url 생성 + @staticmethod + def create_invite_object_and_url(use_type, identifier, code) -> str: + InviteCode.objects.create( + use_type = INVITE_CHOICE_USE_TYPE[use_type], + code = code, + expire_at = set_expire(10080), + identifier = identifier, + ) + return f"https://{settings.DOMAIN_NAME}/invite?t={use_type}&i={identifier}&c={code}" + + # 코드 유효성 검사 + @staticmethod + def check_code(use_type, identifier, code) -> bool: + if InviteCode.objects.filter( + use_type = INVITE_CHOICE_USE_TYPE[use_type], + identifier = identifier, + code = code, + expire_at__gte=now() + ).exists(): + return True + else: + return False + + +class ProjectInviteService(InviteService): + + # 사용자 초대 + @staticmethod + def add_member(invitee: User, work: Project): + return ProjectTeamList.objects.create(user=invitee, project=work) + + diff --git a/codes/urls.py b/codes/urls.py index 2a1a33b..b786798 100644 --- a/codes/urls.py +++ b/codes/urls.py @@ -6,5 +6,5 @@ app_name = 'codes' urlpatterns = [ path('certificate/', CertificationAPIView.as_view()), - # path('invite/', ) + path('invite/', InviteByLinkAPIView.as_view()) ] \ No newline at end of file diff --git a/codes/views.py b/codes/views.py index 1457eb9..7bcf2d5 100644 --- a/codes/views.py +++ b/codes/views.py @@ -14,11 +14,21 @@ from common.models.choiceModels import CertificateCodeUseType from common.utils.codeManger import generate_code -certificate_use_type = { +CERTIFICATE_SERVICE_USE_TYPE = { "phone": SmsService, # "email": EmailService } +INVITE_USE_TYPE = { + "p": { + "word": "project", + "service": ProjectInviteService, + "model": Project, + "team_model": ProjectTeamList + } + # "h": HackathonInviteService +} + class CertificationAPIView(APIView): permission_classes = [AllowAny] @@ -26,7 +36,9 @@ class CertificationAPIView(APIView): @transaction.atomic def post(self, request): use_type = request.query_params.get("type") - serv = certificate_use_type[use_type] + if use_type not in CERTIFICATE_SERVICE_USE_TYPE: + return Response({"message": "Not defined use_type"}, status=status.HTTP_400_BAD_REQUEST) + serv = CERTIFICATE_SERVICE_USE_TYPE[use_type] serializer = CertificateCodeSerializer(data=request.data) if serializer.is_valid(): create_code = generate_code(6) @@ -44,7 +56,9 @@ class CertificationAPIView(APIView): @transaction.atomic def patch(self, request): use_type = request.query_params.get("type") - serv = certificate_use_type[use_type] + if use_type not in CERTIFICATE_SERVICE_USE_TYPE: + return Response({"message": "Not defined use_type"}, status=status.HTTP_400_BAD_REQUEST) + serv = CERTIFICATE_SERVICE_USE_TYPE[use_type] code = request.data.get('code', None) if not code: return Response({"message": "no code"}, status=status.HTTP_400_BAD_REQUEST) @@ -54,3 +68,49 @@ class CertificationAPIView(APIView): return Response({"message": "certificated successfully"}, status=status.HTTP_200_OK) return Response({"message": "wrong code, please retry"}, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class InviteByLinkAPIView(APIView): + + # 링크 초대(복사) + @transaction.atomic + def post(self, request): + use_type = request.query_params.get("type") + if use_type not in INVITE_USE_TYPE: + return Response({"message": "Not defined use_type"}, status=status.HTTP_400_BAD_REQUEST) + + serv = INVITE_USE_TYPE[use_type]['service'] + user = request.user + serializer = InviteCodeSerializer(data=request.data) + if serializer.is_valid(): + work = get_object_or_404(INVITE_USE_TYPE[use_type]['model'], id=serializer.validated_data['identifier']) + if user != work.owner: # 유저 권한 추가될 시 수정 필요 + return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN) + + create_code = generate_code(10) + invite_url = serv.create_invite_object_and_url(use_type, serializer.validated_data['identifier'], create_code) + return Response({"invite_url": invite_url}) + + # 링크 확인 + @transaction.atomic + def patch(self, request): + use_type = request.query_params.get("type") + if use_type not in INVITE_USE_TYPE: + return Response({"message": "Not defined use_type"}, status=status.HTTP_400_BAD_REQUEST) + + serv = INVITE_USE_TYPE[use_type]['service'] + user = request.user + serializer = InviteCodeSerializer(data=request.data) + if serializer.is_valid(): + if not serv.check_code(use_type, serializer.validated_data['identifier'], serializer.validated_data['code']): + return Response({"message": "Not correct or expired code"}, status=status.HTTP_400_BAD_REQUEST) + + work = get_object_or_404(INVITE_USE_TYPE[use_type]['model'], id=serializer.validated_data['identifier']) + filter_dict = {INVITE_USE_TYPE[use_type]['word']:work} + + if INVITE_USE_TYPE[use_type]['team_model'].objects.filter(user=user, **filter_dict).exists(): + return Response({"message": "already invited member"}, status=status.HTTP_400_BAD_REQUEST) + + if serv.add_member(user, work): + return Response({"message": "invite success"}, status=status.HTTP_200_OK) + return Response({"message": "invite failed"}, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/common/models/choiceModels.py b/common/models/choiceModels.py index 0168b50..318884e 100644 --- a/common/models/choiceModels.py +++ b/common/models/choiceModels.py @@ -9,8 +9,8 @@ class CertificateCodeUseType(models.TextChoices): PHONE = 'phone', 'phone' class InviteCodeUseType(models.TextChoices): - PROJECT = '프로젝트', '프로젝트' - HACKATHON = '해커톤', '해커톤' + PROJECT = 'p', 'p' + HACKATHON = 'h', 'h' class NotificationType(models.TextChoices): INVITE = '초대', '초대' diff --git a/config/settings.py b/config/settings.py index 609a60c..b6079fb 100644 --- a/config/settings.py +++ b/config/settings.py @@ -29,6 +29,9 @@ environ.Env.read_env(os.path.join(BASE_DIR, '.env')) SECRET_KEY = env('SECRET_KEY') +DOMAIN_NAME = 'colio.co.kr' +DEV_DOMAIN_NAME = env('DEV_DOMAIN_NAME') + SOLAPI_API_KEY = env('SOLAPI_API_KEY') SOLAPI_API_SECRET = env('SOLAPI_API_SECRET') FROM_PHONE_NUMBER = env('FROM_PHONE_NUMBER')