@extend_schema / 공식문서from django.test import TestCase, TransactionTestCase, SimpleTestCase, LiveServerTestCase
from rest_framework.test import APITestCase, APITransactionTestCase, APILiveServerTestCase
docker compose -f docker-compose.local.yml up db -d
python manage.py showmigrations
python manage.py migrate --fake auth

docker compose -f docker-compose.local.yml down -v db
docker compose -f docker-compose.local.yml up -d db
python manage.py makemigrations users
python manage.py migrate contenttypes
python manage.py migrate sessions
python manage.py migrate users
python manage.py migrate --fake auth
python manage.py showmigrations
Mock API(스펙 기반 가짜 API 서버)
실제 백엔드 없이 API 문서(Spec)만 보고 실제처럼 응답을 돌려주는 서버
| 특징 | 설명 |
|---|---|
| DB 필요 없음 | 가짜 객체를 만들어 serializer로 응답 |
| 프론트엔드 테스트 가능 | 실제로 서버 있는 것처럼 요청 보내서 응답 확인 가능 |
| OpenAPI 문서 자동 생성 | drf-spectacular 이용 |
| 응답 구조는 실제 서버와 동일하게 시뮬레이션 | 나중에 실제 백엔드로 자연스럽게 교체 가능 |
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
image = models.ImageField(upload_to='products')
price = models.IntegerField()
stock = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
write_only: 이미지 업로드할 때만 사용read_only: 프론트에 이미지 경로만 주기 위함from rest_framework import serializers
from apps.products.models import Product
class ProductSerializer(serializers.ModelSerializer):
image = serializers.ImageField(write_only=True)
image_url = serializers.CharField(read_only=True, source='image.url')
class Meta:
model = Product
exclude = ('created_at', 'updated_at')
extra_kwargs = {
'stock': {'write_only': True},
'description': {'write_only': True},
}
class ProductDetailSerializer(serializers.ModelSerializer):
image = serializers.ImageField(write_only=True)
image_url = serializers.CharField(read_only=True, source='image.url')
class Meta:
model = Product
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
from datetime import timedelta
from django.utils import timezone
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from rest_framework import status, parsers
from rest_framework.response import Response
from apps.products.models import Product
from apps.products.serializers import ProductSerializer, ProductDetailSerializer
class ProductListCreateAPIView(APIView):
serializer_class = ProductSerializer
permission_classes = [AllowAny]
# 이미지 및 파일을 요청으로부터 넘겨 받기 위해 MultiPartParser를 명시
parser_classes = [parsers.JSONParser, parsers.MultiPartParser]
# JSONParser는 Content-Type 헤더가 application/json으로 넘어온 json 형태의 요청 본문을 처리하기 위한 Parser 클래스
# MultiPartParser는 Content-Type 헤더가 mulitpart/form-data로 넘어온 폼데이터 형태의 요청 본문을 처리하기 위한 Parser 클래스
@extend_schema(tags=["Products"], summary="상품 등록 API")
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(status=status.HTTP_201_CREATED)
@extend_schema(
# operation_id를 여기서만 직접 선언해주는 이유
# drf-spectacular은 APIView를 사용하는 경우 operation_id를 HTTP method 기준으로만 해석하여 자동 생성하기 때문에 retrieve가 되기 때문입니다.
# ViewSet or GenericView를 사용하는 경우 drf-spectacular는 mixins 클래스의 action 메소드와 url 패턴을 확인하여 operation_id를 생성합니다.
operation_id="v1_products_list",
tags=["Products"],
summary="상품 전체 목록 조회 API",
# drf-spectacular는 APIView의 경우 serializer_class에 지정된 시리얼라이저를 사용하여 스키마를 생성하기 때문에 단일 객체를 응답으로 반환한다고 예제 응답 스키마를 구성합니다.
# list 뷰의 경우 직접 many=True 옵션을 주어 다중 객체를 응답으로 반환한다고 명시합니다.
responses={
200: ProductSerializer(many=True),
}
)
def get(self, request):
mock_data = [
Product(
id=1,
name=f"Mock Product {i}",
description=f"Description for Mock Product {i}",
image="media/products/mock_image.jpg",
price=i * 10000,
stock=10
) for i in range(1, 11)
]
serializer = self.serializer_class(mock_data, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class ProductRetrieveUpdateDestroyAPIView(APIView):
serializer_class = ProductDetailSerializer
permission_classes = [AllowAny]
parser_classes = [parsers.JSONParser, parsers.MultiPartParser]
@extend_schema(tags=["Products"], summary="상품 상세 조회 API")
def get(self, request, product_id):
mock_data = Product(
id=product_id,
name="Mock Product",
description="Description for Mock Product",
image="media/products/mock_image.jpg",
price=10000,
stock=10
)
serializer = self.serializer_class(mock_data)
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(tags=["Products"], summary="상품 정보 수정 API")
def put(self, request, product_id):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
image = serializer.validated_data.get("image", "")
if image:
image_url = f"products/{image.name}"
else:
image_url = f"media/products/mock_image.jpg"
mock_data = Product(
id=product_id,
name=serializer.validated_data.get("name", "Mock Product"),
description=serializer.validated_data.get("description", "Description for Mock Product"),
image=image_url,
price=serializer.validated_data.get("price", 10000),
stock=serializer.validated_data.get("stock", 10),
created_at=timezone.now() - timedelta(days=1),
updated_at=timezone.now(),
)
return Response(self.serializer_class(mock_data).data, status=status.HTTP_200_OK)
@extend_schema(tags=["Products"], summary="상품 삭제 API")
def delete(self, request, product_id):
return Response(status=status.HTTP_204_NO_CONTENT)
from django.urls import path
from apps.products import views
urlpatterns = [
path("", views.ProductListCreateAPIView.as_view(), name="product-create-list"),
path("<int:product_id>/", views.ProductRetrieveUpdateDestroyAPIView.as_view(), name="product-detail"),
]
from django.conf import settings
from django.conf.urls.static import static
from django.urls import URLPattern, URLResolver, include, path
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
urlpatterns: list[URLPattern | URLResolver] = [
path("api/v1/products/", include("apps.products.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if "debug_toolbar" in settings.INSTALLED_APPS:
urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))]
if "drf_spectacular" in settings.INSTALLED_APPS:
urlpatterns += [
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
]
.env 추가하기[문제 원인 & 해결 요약 — 3가지]1) Django의 DATABASE 설정이 docker-compose 기준(HOST=db)인데,
단독 docker run으로 실행한 Postgres는 해당 이름/네트워크가 아니어서
Django가 DB를 찾지 못해 connection refused 발생.
2) Django가 PostgreSQL(DB)에 접속하려고 했는데, localhost:5432에서 실행 중인 Postgres 서버가 아예 없음. → 그래서 showmigrations가 DB에 연결을 못하고 터짐.
3) docker compose -f docker-compose.local.yml up db -d 로 실행하면
compose 네트워크·서비스 이름(db)·환경변수가 Django 설정과 정확히 일치하여
Django → Postgres 연결이 정상적으로 이루어져 문제 해결됨.