diff --git a/api/urls.py b/api/urls.py index 8bd4d55..79de0d5 100644 --- a/api/urls.py +++ b/api/urls.py @@ -2,7 +2,7 @@ urlpatterns = [ path( - "/user", + "user/", include("api.users.urls"), name="user", ), diff --git a/api/users/serializers.py b/api/users/serializers.py index 4466567..8503cc3 100644 --- a/api/users/serializers.py +++ b/api/users/serializers.py @@ -2,6 +2,8 @@ class SendVerifyCodeSerializer(serializers.Serializer): + """이메일 인증 요청""" + email = serializers.EmailField( help_text="이메일 주소", ) diff --git a/api/users/urls.py b/api/users/urls.py index 267241b..2d64749 100644 --- a/api/users/urls.py +++ b/api/users/urls.py @@ -1,11 +1,11 @@ from django.urls import path -from api.users.views import EmailRequestView +from api.users.views import EmailVerificationAPIView urlpatterns = [ path( - "/email/send", - EmailRequestView.as_view(), - name="email-send", + "email-verification", + EmailVerificationAPIView.as_view(), + name="email-verification", ), ] diff --git a/api/users/views.py b/api/users/views.py index 4638ecd..5d87521 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -6,22 +6,26 @@ from api.users.schema import email_send_schema from api.users.serializers import SendVerifyCodeSerializer -from apps.users.services import UserVerificationService +from apps.users.services import EmailVerificationService from core.exceptions import InvalidRequestError from core.throttles import TwoRequestPerOneMinuteAnonRateThrottle -class EmailRequestView(APIView): +class EmailVerificationAPIView(APIView): + """이메일 인증""" + permission_classes = [AllowAny] throttle_classes = [TwoRequestPerOneMinuteAnonRateThrottle] + email_verification_service = EmailVerificationService() + @email_send_schema def post(self, request: Request) -> Response: input_serializer = SendVerifyCodeSerializer(data=request.data) if not input_serializer.is_valid(): raise InvalidRequestError - UserVerificationService().send_verification_code( + self.email_verification_service.send_verification_email( email=input_serializer.validated_data["email"], ) diff --git a/apps/users/migrations/0002_alter_user_is_active.py b/apps/users/migrations/0002_alter_user_is_active.py new file mode 100644 index 0000000..be7685d --- /dev/null +++ b/apps/users/migrations/0002_alter_user_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0 on 2025-12-21 06:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="is_active", + field=models.BooleanField(default=False, verbose_name="계정 활성화 여부"), + ), + ] diff --git a/apps/users/models.py b/apps/users/models.py index 4aae45b..b995e41 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -15,5 +15,10 @@ class User(TimeStampedModel, AbstractUser): null=True, ) + is_active = models.BooleanField( + "계정 활성화 여부", + default=False, + ) + class Meta: db_table = "user" diff --git a/apps/users/services.py b/apps/users/services.py index c00daa9..079f53e 100644 --- a/apps/users/services.py +++ b/apps/users/services.py @@ -1,16 +1,20 @@ from django.db import transaction from apps.users.models import User -from apps.users.tasks import send_email_verify_code +from apps.users.tasks import send_verification_code -class UserVerificationService: +class EmailVerificationService: @transaction.atomic - def send_verification_code(self, email: str) -> None: + def send_verification_email( + self, + email: str, + ) -> None: + """인증 메일 발송""" + User.objects.get_or_create( email=email, - defaults={"is_active": False}, ) - send_email_verify_code.delay(email=email) + send_verification_code.delay(email=email) diff --git a/apps/users/tasks.py b/apps/users/tasks.py index 1f4ae13..24f1ff5 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -9,13 +9,15 @@ from core.cache.prefix import CacheKeyPrefix -@shared_task() -def send_email_verify_code(user_email: str) -> None: +@shared_task +def send_verification_code(email: str) -> None: + """인증 메일 전송""" + code = random.randint(100000, 999999) Cache.set( prefix=CacheKeyPrefix.email_verification_code, - key=user_email, + key=email, value=code, timeout=60 * 10, ) @@ -23,19 +25,19 @@ def send_email_verify_code(user_email: str) -> None: html_content = render_to_string( template_name="verify_code.html", context={ - "recipient_name": user_email, + "recipient_name": email, "verification_code": code, }, ) - email = EmailMultiAlternatives( + email_message = EmailMultiAlternatives( subject=f"Jusicool 메일 인증 코드", body=html_content, - to=[user_email], + to=[email], from_email=settings.EMAIL_HOST_USER, ) - email.attach_alternative( + email_message.attach_alternative( html_content, "text/html", ) - email.send() + email_message.send() diff --git a/apps/users/tests/test_services.py b/apps/users/tests/test_services.py index fca53db..5f9c341 100644 --- a/apps/users/tests/test_services.py +++ b/apps/users/tests/test_services.py @@ -3,11 +3,11 @@ import pytest from apps.users.factories import UserFactory -from apps.users.services import UserVerificationService +from apps.users.services import EmailVerificationService @pytest.mark.django_db -class TestUserVerificationService: +class TestEmailVerificationService: @pytest.fixture(autouse=True) def mock_exist_user(self) -> UserFactory: @@ -16,15 +16,15 @@ def mock_exist_user(self) -> UserFactory: is_active=True, ) - @patch("apps.users.services.send_email_verify_code.delay") - def test_send_verification_code( + @patch("apps.users.services.send_verification_code.delay") + def test_send_verification_email( self, mock_task, ): """메일 전송 task를 호출한다.""" # Action - UserVerificationService().send_verification_code(email="new@test.com") + EmailVerificationService().send_verification_email(email="new@test.com") # Assert mock_task.assert_called_once_with(email="new@test.com") diff --git a/apps/users/tests/test_tasks.py b/apps/users/tests/test_tasks.py index f242d84..c08660f 100644 --- a/apps/users/tests/test_tasks.py +++ b/apps/users/tests/test_tasks.py @@ -1,14 +1,14 @@ from unittest.mock import patch -from apps.users.tasks import send_email_verify_code +from apps.users.tasks import send_verification_code from core.cache.prefix import CacheKeyPrefix -class TestSendEmailVerifyCode: +class TestSendVerificationCode: @patch("apps.users.tasks.Cache.set") @patch("apps.users.tasks.random.randint") - def test_send_email_verify_code( + def test_send_verification_code( self, mock_randint, mock_cache_set, @@ -21,7 +21,7 @@ def test_send_email_verify_code( mock_randint.return_value = mock_code # Action - send_email_verify_code(user_email=user_email) + send_verification_code(email=user_email) # Assert mock_cache_set.assert_called_once_with( diff --git a/internal_api/urls.py b/internal_api/urls.py index fbfc084..beaa627 100644 --- a/internal_api/urls.py +++ b/internal_api/urls.py @@ -6,14 +6,18 @@ ) urlpatterns = [ - path("/schema", SpectacularAPIView.as_view(), name="schema"), path( - "/swagger", + "schema", + SpectacularAPIView.as_view(), + name="schema", + ), + path( + "swagger", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui", ), path( - "/redoc", + "redoc", SpectacularRedocView.as_view(url_name="schema"), name="redoc", ), diff --git a/jusicool/urls.py b/jusicool/urls.py index 5b84278..92de682 100644 --- a/jusicool/urls.py +++ b/jusicool/urls.py @@ -2,7 +2,18 @@ from django.urls import include, path urlpatterns = [ - path("jadmin/", admin.site.urls), - path("api", include("api.urls"), name="api"), - path("_api", include("internal_api.urls"), name="internal_api"), + path( + "jadmin/", + admin.site.urls, + ), + path( + "api/", + include("api.urls"), + name="api", + ), + path( + "_api/", + include("internal_api.urls"), + name="internal_api", + ), ]