
[2025.12.20] 질문 수정 api SpecAPI 작성
[2025.12.22] 질문 수정 PR 리뷰로 인한 전체적으로 갈아엎기
[2025.12.23] 질문 수정 PR 리뷰로 인한 대대적인 충돌 해결
from rest_framework import serializers
class QuestionUpdateRequestSpecSerializer(serializers.Serializer):
title = serializers.CharField()
content = serializers.CharField()
category_id = serializers.IntegerField()
image_urls = serializers.ListField(
child=serializers.URLField(),
required=False,
)
class QuestionUpdateResponseSpecSerializer(serializers.Serializer):
question_id = serializers.IntegerField()
updated_at = serializers.DateTimeField()
from django.utils import timezone
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
extend_schema,
OpenApiParameter,
)
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.qna.spec.question.spec_question_update.serializers import (
QuestionUpdateRequestSpecSerializer,
QuestionUpdateResponseSpecSerializer,
)
class QuestionUpdateSpecAPIView(APIView):
"""
질문 수정 API (Spec)
- 실제 DB 반영 없이 mock 응답만 반환
"""
authentication_classes = [] # Spec API는 인증 처리 X
permission_classes = []
@extend_schema(
tags=["Questions"],
summary="질문 수정 API (Spec)",
description="질문 ID를 기준으로 질문을 수정합니다. (Mock API)",
parameters=[
OpenApiParameter(
name="question_id",
type=OpenApiTypes.INT,
location=OpenApiParameter.PATH,
description="수정할 질문 ID",
required=True,
),
],
request=QuestionUpdateRequestSpecSerializer,
responses={
200: QuestionUpdateResponseSpecSerializer,
400: OpenApiTypes.OBJECT,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
404: OpenApiTypes.OBJECT,
},
)
def put(self, request: Request, question_id: int) -> Response:
# 400 mock
if question_id <= 0:
raise ValidationError("유효하지 않은 질문 수정 요청입니다.")
serializer = QuestionUpdateRequestSpecSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# mock success response
return Response(
{
"question_id": question_id,
"updated_at": timezone.now(),
},
status=status.HTTP_200_OK,
)
class QuestionUpdatePermissionTests(TestCase):
def setUp(self) -> None:
self.permission = QuestionUpdatePermission()
self.student = User.objects.create_user(
email="student@test.com",
password="test1234",
name="수강생",
role=RoleChoices.ST,
phone_number="010-0000-0000",
gender="M",
birthday="2000-01-01",
)
self.admin = User.objects.create_user(
email="admin@test.com",
password="test1234",
name="관리자",
role=RoleChoices.AD,
phone_number="010-1111-1111",
gender="M",
birthday="2000-01-01",
)
def test_permission_unauthenticated(self):
request = type("Request", (), {"user": AnonymousUser()})()
with self.assertRaises(QuestionUpdateNotAuthenticated):
self.permission.has_permission(request, None)
def test_permission_student_allowed(self):
request = type("Request", (), {"user": self.student})()
self.assertTrue(self.permission.has_permission(request, None))
def test_permission_not_student(self):
request = type("Request", (), {"user": self.admin})()
self.assertFalse(self.permission.has_permission(request, None))
# 비로그인 사용자
# 검증: 로그인이 안 된 상태에서 has_permission() 호출 시 401 예외가 발생해야 한다
def test_permission_unauthenticated(self):
request = type("Request", (), {"user": AnonymousUser()})()
with self.assertRaises(QuestionUpdateNotAuthenticated):
self.permission.has_permission(request, None)
# 수강생(ST)
# 로그인 O / 역할 = ST / True 반환
def test_permission_student_allowed(self):
request = type("Request", (), {"user": self.student})()
self.assertTrue(self.permission.has_permission(request, None))
# 수강생이 아닌 사용자
# 로그인 O / 역할 = AD / False 반환 (403)
def test_permission_not_student(self):
request = type("Request", (), {"user": self.admin})()
self.assertFalse(self.permission.has_permission(request, None))
2025.12.22
# QuestionAPIView.get
queryset = get_question_list(**query_serializer.validated_data)
paginator = QuestionPageNumberPagination()
page = paginator.paginate_queryset(queryset, request)
serializer = QuestionListSerializer(page, many=True)
return paginator.get_paginated_response(serializer.data)
get_question_list() → QuerySet 반환
paginate_queryset() → 빈 QuerySet이어도 예외 없음
serializer(page, many=True) → page == [] 이면 []
get_paginated_response([]) → 정상 응답
base_qs = Question.objects.select_related(...).annotate(...)
qs = filter_by_answered(base_qs, answered)
qs = filter_by_category(qs, category_id)
qs = filter_by_search(qs, search_keyword)
qs = filter_by_sort(qs, sort)
get_paginated_response([]) 결과는 항상 이 형태{
"count": 0,
"next": null,
"previous": null,
"results": []
}
| 역할 | 담당 주체 |
|---|---|
| 빈 QuerySet 처리 | ✅ PageNumberPagination |
| count 계산 | ✅ PageNumberPagination |
| next / previous 계산 | ✅ PageNumberPagination |
| results 배열 생성 | ✅ PageNumberPagination |
| View | ❌ 관여 안 함 |
| Serializer | ❌ 관여 안 함 |
2025.12.23