From 17bda45cb7a5ae4519383e130a882e36ec6fd179 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 May 2025 00:00:30 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#25]=20solapi=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | Bin 1162 -> 1578 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8ae4bec99de507bfdb28500a99150d05f631914c..d9f0322b7bfe24eddd65cc73dc5f661fabaae99c 100644 GIT binary patch delta 410 zcmZ9IKWoBJ6va=}rP2;Xx)sF1(UL#5=-|+?n;_0XVyXd&p>fcuyN>T0IJ!CZQ}~Ul z=e!U>xjbINJNI|bJ==%FWApq!LCRWUg$YlLDLTA2$oQR`XQ$DUVuOU50jrBQ^m$x7 zp^i@sF*1(xa(vxa#&dWqsaz^Nd|Y*NGFN0qr2LR9jb~p)6RA|Jul+uHqL{zR=_{jy zDIKPyB&4X{w=+cPTEe|1yqP9-fWZ7#cR@=!9jKI?5B&Wv(XJvfXoOiys=3MPW vN+qF(88gzI+ms^fY03X`b7fTIQzTjRgxq^gZEQO=TLkq;?^y@FY1iTlRQgC| delta 27 jcmZ3*)5Xd3|KCQxJxr5Rm`_b^U}>7{z^XL)3~L+!q(=)M From 46f175d44627afd2e53cf7b7a9c3afaf2065ba4e Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 May 2025 00:01:40 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#25]=20solapi=20?= =?UTF-8?q?=ED=82=A4=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=88=98=EC=8B=A0=20=EC=A0=84=ED=99=94=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/settings.py b/config/settings.py index 41cf535..609a60c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -29,6 +29,10 @@ environ.Env.read_env(os.path.join(BASE_DIR, '.env')) SECRET_KEY = env('SECRET_KEY') +SOLAPI_API_KEY = env('SOLAPI_API_KEY') +SOLAPI_API_SECRET = env('SOLAPI_API_SECRET') +FROM_PHONE_NUMBER = env('FROM_PHONE_NUMBER') + DEBUG = env.bool('DEBUG') ALLOWED_HOSTS = ['*'] From 185e23b81312c668883740daf84401d2ee3b9a55 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 May 2025 00:02:39 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#25]=20code=20api=20ur?= =?UTF-8?q?l=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/urls.py b/config/urls.py index b17ad1b..9a6a6f4 100644 --- a/config/urls.py +++ b/config/urls.py @@ -7,7 +7,7 @@ from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('api/user/', include('users.urls')), - # path('api/code/', include('codes.urls')), + path('api/code/', include('codes.urls')), path('api/portfolio/', include('portfolios.urls')), path('api/project/', include('projects.urls')), path('api/notification/', include('notifications.urls')), From 5352f16eebe96d8468171b3b6e8e20708223ae0f Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 May 2025 00:03:22 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix:=20[#25]=20certifi?= =?UTF-8?q?catecodeusetype=20=EA=B0=92=20=EC=98=81=EC=96=B4=EB=A1=9C=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 73b72d3..0168b50 100644 --- a/common/models/choiceModels.py +++ b/common/models/choiceModels.py @@ -5,8 +5,8 @@ class GenderChoices(models.TextChoices): WOMAN = '여', '여성' class CertificateCodeUseType(models.TextChoices): - EMAIL = '이메일', '이메일' - PHONE = '휴대폰', '휴대폰' + EMAIL = 'email', 'email' + PHONE = 'phone', 'phone' class InviteCodeUseType(models.TextChoices): PROJECT = '프로젝트', '프로젝트' From 28a18f33b1f397e2df9e14cab4368519ec38e536 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 May 2025 00:04:18 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#25]=20=ED=9C=B4?= =?UTF-8?q?=EB=8C=80=ED=8F=B0=20=EC=9D=B8=EC=A6=9D=20serializer=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codes/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 codes/serializers.py diff --git a/codes/serializers.py b/codes/serializers.py new file mode 100644 index 0000000..ea32359 --- /dev/null +++ b/codes/serializers.py @@ -0,0 +1,10 @@ +from django.utils.timezone import now + +from .models import * +from rest_framework import serializers +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) From 629cdcb61b2de1a13d3f4d29359e058473807163 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 May 2025 00:05:29 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#25]=20=ED=9C=B4?= =?UTF-8?q?=EB=8C=80=ED=8F=B0=20=EC=9D=B8=EC=A6=9D=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=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 | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 codes/services.py diff --git a/codes/services.py b/codes/services.py new file mode 100644 index 0000000..4449084 --- /dev/null +++ b/codes/services.py @@ -0,0 +1,82 @@ +import random +import string +import requests + +from django.db import IntegrityError +from django.conf import settings +from django.utils.timezone import now + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status + +from .models import CertificationCode + +from common.models.choiceModels import CertificateCodeUseType +from common.utils.codeManger import set_expire + +from solapi import SolapiMessageService +from solapi.model import RequestMessage + +# from .schemas import send_sms_post_schema # Swagger나 drf-spectacular 등에 사용되는 데코레이터 + +class CertificateService: + @staticmethod + def send(code, identifier): + pass + + # 유효시간 5분 설정해서 저장, 이전 같은 번호 sms 요청 값들 is_used=True로 바꾸기 + @staticmethod + def save_certificate_info(use_type: CertificateCodeUseType, code, identifier): + try: + befores = CertificationCode.objects.filter( + use_type=use_type, + identifier=identifier, + is_used=False + ).update(is_used=True) + instance = CertificationCode.objects.create( + use_type = use_type, + code = code, + expire_at = set_expire(5), + identifier = identifier + ) + return True + except IntegrityError as e: + return False + + # code 체크 후 is_used=True로 설정 + @staticmethod + def check_code(use_type: CertificateCodeUseType, code, identifier): + if check := CertificationCode.objects.filter( + use_type=use_type, + identifier=identifier, + is_used=False, + expire_at__gte=now() + ).order_by('-created_at').first(): + + if check.code == code: + check.is_used=True + check.save() + return True + return False + +class SmsService(CertificateService): + # 같은 전번에 대해 요청 텀 설정은 views 단에서 하자. + @staticmethod + def send(code, identifier): + message_service = SolapiMessageService( + api_key = settings.SOLAPI_API_KEY, api_secret = settings.SOLAPI_API_SECRET + ) + + message = RequestMessage( + from_ = settings.FROM_PHONE_NUMBER, + to = identifier, + text = "colio 서비스 회원가입 인증번호는 " + code +" 입니다." + ) + + try: + res = message_service.send(message) + return True + except Exception as e: + print(f"메시지 발송 실패: {str(e)}") + return False \ No newline at end of file From 709f6a8c2982bf15a03de387c60c06af67e4ed56 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 May 2025 00:05:56 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#25]=20=ED=9C=B4?= =?UTF-8?q?=EB=8C=80=ED=8F=B0=20=EC=9D=B8=EC=A6=9D=20api=20url=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codes/urls.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 codes/urls.py diff --git a/codes/urls.py b/codes/urls.py new file mode 100644 index 0000000..2a1a33b --- /dev/null +++ b/codes/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .views import * + +app_name = 'codes' + +urlpatterns = [ + path('certificate/', CertificationAPIView.as_view()), + # path('invite/', ) +] \ No newline at end of file From d7e5b622193e0bd456d1c5f099a8e9cd7cafdba6 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 May 2025 00:06:38 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#25]=20usetype?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=9D=B8=EC=A6=9D=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=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 | 57 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/codes/views.py b/codes/views.py index 91ea44a..1457eb9 100644 --- a/codes/views.py +++ b/codes/views.py @@ -1,3 +1,56 @@ -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.permissions import AllowAny, IsAuthenticated + +from rest_framework import status +from rest_framework.response import Response +from django.core.mail import EmailMessage +from django.db import transaction + +from .serializers import * +from .services import * +from common.models.choiceModels import CertificateCodeUseType +from common.utils.codeManger import generate_code + + +certificate_use_type = { + "phone": SmsService, + # "email": EmailService +} + +class CertificationAPIView(APIView): + permission_classes = [AllowAny] + + # 인증 발송 + @transaction.atomic + def post(self, request): + use_type = request.query_params.get("type") + serv = certificate_use_type[use_type] + serializer = CertificateCodeSerializer(data=request.data) + if serializer.is_valid(): + create_code = generate_code(6) + if serv.save_certificate_info(use_type, create_code, serializer.validated_data['identifier']): + if serv.send(create_code, serializer.validated_data['identifier']): + return Response({'message': "success send and save"}) + else: # 전송 실패 + return Response({"message": "failed send"}) + else: # 코드 저장 실패 + return Response({'message': "failed save"}, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + # 인증 확인 + @transaction.atomic + def patch(self, request): + use_type = request.query_params.get("type") + serv = certificate_use_type[use_type] + code = request.data.get('code', None) + if not code: + return Response({"message": "no code"}, status=status.HTTP_400_BAD_REQUEST) + serializer = CertificateCodeSerializer(data=request.data) + if serializer.is_valid(): + if serv.check_code(use_type, code, serializer.validated_data['identifier']): + 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)