2025/12/08 MainProject - 1

김기훈·2025년 12월 8일

TIL

목록 보기
77/194

오늘 학습 내용 ✅

  • SpecAPI
    • 실기능은 하지 않지만 요청 / 응답 스키마를 구성하고 있는 깡통API
  • git
    • 디벨롭에서 분기된 a 브랜치에서 작업 pr
      • 다른 api작업을 해야할 경우 -> develop에서 분기
      • a 에서 분기해도 괜찮은 경우
        • a브랜치에서 작업한 내역이 신규 작업에 필요한 경우 가능
  • drf - spectacular
    • View / url / serializer / permission 등
    • drf의 기능 파일을 쭉 읽어와서 swagger 문서 스키마를 동적으로 생성
  • startapp app_name 할 경우 app 디렉토리에 디렉토리를 만들고 startapp 해야 함
  • django test

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

  • DB 날리고 다시

  • 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

    • 마이그레이션 적용내역 확인

17:30 과제

  • 회원 가입 spec api 구현, 회원정보 조회 spec api 구현

    • 모델을 DB에 Create 하거나 조회할 필요도없음

SpecAPI

  • Mock API(스펙 기반 가짜 API 서버)

    • 실제 백엔드 없이 API 문서(Spec)만 보고 실제처럼 응답을 돌려주는 서버

      특징설명
      DB 필요 없음가짜 객체를 만들어 serializer로 응답
      프론트엔드 테스트 가능실제로 서버 있는 것처럼 요청 보내서 응답 확인 가능
      OpenAPI 문서 자동 생성drf-spectacular 이용
      응답 구조는 실제 서버와 동일하게 시뮬레이션나중에 실제 백엔드로 자연스럽게 교체 가능

  • 예시

  • apps/products/models.py

    • DB에서 가져오진 않지만, Mock 객체를 만들 때 구조를 맞추려고 사용
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
  • apps/products/serializers.py

    • image는 write_only: 이미지 업로드할 때만 사용
    • image_url은 read_only: 프론트에 이미지 경로만 주기 위함
      • 응답엔 실제 이미지 파일이 아니라 URL만 내려줌
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')
  • apps/products/views.py

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)

  • apps/products/urls.py

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"),
]
  • config/urls.py

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


과제 2


Onboarding

  • 1. 프로젝트 세팅

    • 1-1. 프로젝트 루트 디렉터리 생성
    • 1-2. Git init & Git pull
    • 1-3. .env 추가하기
    • 1-4. 가상환경 설정 및 의존성 패키지 설치
    • 1-5. Docker 설치
    • 1-6. Docker 와 Docker Compose 를 활용하여 로컬 환경에서 서버 실행하기
  • 2. 팀 별 업무 진행 방식


파트 변경

  • 질문 등록("/api/v1/qna/questions")

    • 웹사이트 이용자 중 수강생 권한을 가진 로그인 유저는 질의응답 메뉴로 접속하여 질문 등록 가능
    • 질문 등록 시 입력 항목
      • 제목
      • 질문 내용 ( 마크다운 문법 사용 가능, 이미지 첨부 가능 )
      • 카테고리 (대분류 > 중분류 > 소분류)
      • 내용에 첨부된 이미지들

  • 질의응답 목록 조회("/api/v1/qna/questions")

    • 모든 웹사이트 이용자는 질의응답 메뉴로 접속하여 등록된 질의응답 목록을 조회 가능
    • 질의응답 목록 조회 시 상단의 탭을 이용하여 답변 작성 여부에 따라 목록을 조회할 수 있음
      • 추가로 질문 카테고리별로 필터링을 적용 가능
    • 질의응답 목록 조회 시 검색 기능을 활용하여 원하는 질의응답 항목을 찾기 가능
    • 조회된 질의응답 목록은 최신순으로 정렬되어야하며 페이지네이션이 적용
    • 목록에서 각 항목은 카드 형태로 노출
    • 질의응답 목록에서 조회가능한 항목 ( 각 카드에 포함되어야 하는 항목 )

      • 질의응답 카테고리 ( 대분류 > 중분류 > 소분류 형태 )
      • 작성자 정보
        • 프로필 이미지
        • 닉네임
      • 질의응답 제목
      • 질문 내용
      • 답변 수
      • 조회수
      • 질문 작성일시
      • 질문 내용에 포함된 이미지의 썸네일 이미지"

  • 질의응답 상세 조회("/api/v1/qna/questions/{question_id}")

    • 모든 웹사이트 이용자는 조회된 질의응답 목록 중에서 특정 항목을 클릭하여 상세 조회 가능
    • 특정 항목을 클릭 시 상세조회 페이지로 이동
    • 상세 조회 페이지에서 확인 가능한 항목

      • 질문 제목
      • 질문 내용
      • 질문 내용에 첨부된 이미지
      • 질문 작성자 정보
        • 프로필 썸네일 이미지
        • 닉네임
      • 질문 카테고리 정보
        • 대, 중, 소분류 카테고리 이름
      • 질문 조회수
      • 질문 작성일시
      • 답변 목록
        • 답변 작성자 정보
          • 프로필 썸네일 이미지
          • 닉네임
        • 답변 내용
        • 답변 작성일시
        • 답변 채택 여부
        • 답변에 대한 댓글 목록
          • 댓글 작성자 정보
            • 프로필 썸네일 이미지
            • 유저 닉네임
          • 댓글 내용
          • 댓글 작성일시

  • 질문 수정("/api/v1/qna/questions/{question_id}")

    • 웹사이트 이용자 중 수강생 권한을 가진 로그인 유저는 질의응답 메뉴로 접속하여
      • 자신이 작성한 질문 내용을 수정 가능
    • 질의응답 상세 페이지 내에 위치한 수정하기 버튼을 클릭하여 해당 질문 내용을 수정 가능
    • 수정하기 버튼은 작성자 본인외에 노출되지 않아야 함
    • 수정 가능한 항목

      • 제목
      • 질문 내용 ( 마크다운 문법 사용 가능, 이미지 첨부 가능 )
      • 카테고리
      • 내용에 첨부된 이미지들"

새롭게 알게된 내용 ✅

  • warp 설치 사용 권장
    • ai기반 터미널
  • OWASP

어려운 내용(추가 학습 필요) ✅

오늘 발생한 문제(발생 했다면) ✅

  • [문제 원인 & 해결 요약 — 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 연결이 정상적으로 이루어져 문제 해결됨.


  • DB_HOST = localhost
    • postgres://postgres:pw1234@localhost:5432/ozcoding_externship 연결시도
  • 문제 1
    • docker에서 postgres 컨테이너가 충돌 상태였음
    • 그러니 localhost:5432 에 접속하면 → 접속 거절(Connection Refused)
  • 문제 2
    • docker-compose 환경에서는 DB_HOST=postgres 를 사용해야 정상 작동
  • 해결
    • docker compose up db -d
    • docker compose는 postgres 라는 서비스명을 가진 Postgres 컨테이너를 생성
    • Django가 실제로는 localhost를 보고 있었지만,
      • compose 환경에서는 DB가 정상적으로 뜨니 충돌이 사라진 것
profile
안녕하세요.

0개의 댓글