From 5e9c0439741641606c2045c47e7b0c8afb255686 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 16 May 2025 03:32:51 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#56]=20settings.py?= =?UTF-8?q?=EC=97=90=20=EB=8F=84=EB=A9=94=EC=9D=B8,=20=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=B3=80=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 3 +++ 1 file changed, 3 insertions(+) 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') From 0f9a994fbcccdbcc86a6bf408d0ccee0f7867f5d Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 16 May 2025 03:33:33 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix:=20[#56]=20InviteC?= =?UTF-8?q?odeUseType=20choice=20=ED=95=84=EB=93=9C=20=EA=B0=92=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 --- common/models/choiceModels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 = '초대', '초대' From 043ac8841820602204f167535f9c6800a3ab34c6 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 16 May 2025 03:34:38 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#56]=20contenttype=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=96=88=EB=8B=A4=EA=B0=80=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codes/models.py | 6 ++++++ 1 file changed, 6 insertions(+) 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) + From e55457a45f0f38ab65f4a6f367f5282d8ba20cd0 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 16 May 2025 03:35:13 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#56]=20invitecodeseria?= =?UTF-8?q?lizer=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codes/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) 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 From 73248e0fa62f7ccc923a82e5513a65d5a651841c Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 16 May 2025 03:39:13 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#56]=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=EA=B4=80=EB=A0=A8=20=EC=84=9C=EB=B9=84=EC=8A=A4(url?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1,=20=EC=BD=94=EB=93=9C=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=EA=B2=80=EC=82=AC,=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=B4=88=EB=8C=80)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codes/services.py | 57 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) 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) + + From 877e1cbf77296c67b3d3bf61f92174f02763ec12 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 16 May 2025 03:39:43 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#56]=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=B4=88=EB=8C=80=20=EA=B4=80=EB=A0=A8=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20url=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codes/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 418d30895226bbb5cc03a70e8400c8a1907cdfd4 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Fri, 16 May 2025 03:40:22 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#56]=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=B4=88=EB=8C=80,=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=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 --- codes/views.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 3 deletions(-) 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