Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

urlpatterns = [
path(
"/user",
"user/",
include("api.users.urls"),
name="user",
),
Expand Down
2 changes: 2 additions & 0 deletions api/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@


class SendVerifyCodeSerializer(serializers.Serializer):
"""이메일 인증 요청"""

email = serializers.EmailField(
help_text="이메일 주소",
)
8 changes: 4 additions & 4 deletions api/users/urls.py
Original file line number Diff line number Diff line change
@@ -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",
),
]
10 changes: 7 additions & 3 deletions api/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서비스 인스턴스를 클래스 속성으로 정의하면 모든 요청에서 동일한 인스턴스를 공유하게 됩니다. 상태를 가지지 않는 서비스라면 문제없지만, Django의 일반적인 패턴은 메서드 내에서 인스턴스를 생성하거나 의존성 주입을 사용하는 것입니다. 매 요청마다 새로운 인스턴스가 필요한 경우 post 메서드 내에서 인스턴스를 생성하는 것을 고려하세요.

Copilot uses AI. Check for mistakes.

@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"],
)

Expand Down
18 changes: 18 additions & 0 deletions apps/users/migrations/0002_alter_user_is_active.py
Original file line number Diff line number Diff line change
@@ -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="계정 활성화 여부"),
),
]
5 changes: 5 additions & 0 deletions apps/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ class User(TimeStampedModel, AbstractUser):
null=True,
)

is_active = models.BooleanField(
"계정 활성화 여부",
default=False,
)

class Meta:
db_table = "user"
14 changes: 9 additions & 5 deletions apps/users/services.py
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 10 additions & 8 deletions apps/users/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,35 @@
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,
)

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],
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변수명 충돌이 발생합니다. 함수 파라미터 emailEmailMultiAlternatives 인스턴스를 저장하는 변수 email_multi_alternative이 혼재되어 있습니다. 36번째 줄의 to=[email]에서 email은 함수 파라미터를 가리키지만, 이미 33번째 줄에서 다른 목적의 변수명으로 사용되어 혼란을 야기할 수 있습니다. 변수명 email_multi_alternativeemail_message로 변경하여 명확성을 높이는 것을 권장합니다.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

from_email=settings.EMAIL_HOST_USER,
)
email.attach_alternative(
email_message.attach_alternative(
html_content,
"text/html",
)
email.send()
email_message.send()
10 changes: 5 additions & 5 deletions apps/users/tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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="[email protected]")
EmailVerificationService().send_verification_email(email="[email protected]")

# Assert
mock_task.assert_called_once_with(email="[email protected]")
8 changes: 4 additions & 4 deletions apps/users/tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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(
Expand Down
10 changes: 7 additions & 3 deletions internal_api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
Expand Down
17 changes: 14 additions & 3 deletions jusicool/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
]