2025/12/12 MainProject - 5

김기훈·2025년 12월 12일

TIL

목록 보기
81/194

오늘 학습 내용 ✅

  • 자주 사용하는 git 명령어 및 코드 리펙터 코드 정리


  • 질문등록 api

    • 변경된 프로젝트 구조에맞게 분리
      • “View / Serializer / Service / Permission”
  • Permission

    • 권한 / 중복 제목
  • Servier

    • DB 생성 로직
  • Serializer

    • 검증 + 데이터 정리만 담당
  • View

    • HTTP 책임만 유지

  • *

def create_question(
    *,
    author: User,
    title: str,
    content: str,
    category: QuestionCategory,
    image_urls: List[str],
) -> Question:
  • * 뒤에 오는 모든 파라미터는 위치 인자(positional)로 받을 수 없고
    • 반드시 키워드 인자(keyword)로만 전달해야 한다
-----------------------------------
def f(a, b, c):
    ...

f(1, 2, 3)          # OK (순서 중요)
f(a=1, b=2, c=3)   # OK
-----------------------------------
def f(*, a, b, c):
    ...

f(a=1, b=2, c=3)   # OK
f(1, 2, 3)         # ❌ 에러
-----------------------------------

pr 수정사항

  • 권한 검증은 뷰나 퍼미션으로 옮기기
  • 시리얼라이저는 데이터 유효성 검사만
  • 뷰 로직의 에러타입을 validationerror 구조로 변경
  • 제목 중복여부는 도메인 규칙 서비스에서 처리
  • 카테고리 에러도 서비스로

ValidationError 구조

  • 원래 사용하던 방식

    • 문제

      • DRF 표준 구조가 아님
      • View가 도메인 규칙을 알게 됨
      • 문자열 오타 → 런타임 버그
      • 테스트도 "type" 문자열에 의존
raise serializers.ValidationError({"type": "title_conflict"})

# view에서 
error_type = e.detail.get("type")

if error_type == "title_conflict":
    ...
# DRF가 기대하는 구조
ValidationError({
    "field_name": ["error message"]
})

ValidationError({
    "non_field_errors": ["error message"]
})
  • ValidationError 구조로 변경

# 기존
raise serializers.ValidationError({"type": "title_conflict"})

# 변경
raise serializers.ValidationError({
    "title": ["중복된 질문 제목이 이미 존재합니다."]
})

## 의미
title 필드와 관련된 에러 / View는 타입 몰라도 됨 / DRF 표준 에러 응답 가능
# 기존
raise serializers.ValidationError({"type": "category_not_found"})

# 변경
raise serializers.ValidationError({
    "category": ["선택한 카테고리를 찾을 수 없습니다."]
})
except serializers.ValidationError as e:
    if "title" in e.detail:
        return Response(
            {"error_detail": e.detail["title"][0]},
            status=status.HTTP_409_CONFLICT,
        )

    if "category" in e.detail:
        return Response(
            {"error_detail": e.detail["category"][0]},
            status=status.HTTP_404_NOT_FOUND,
        )

    return Response(
        {"error_detail": "유효하지 않은 질문 등록 요청입니다."},
        status=status.HTTP_400_BAD_REQUEST,
    )

카테고리 (대분류 > 중분류 > 소분류)

class QuestionCategory(TimeStampedModel):
    parent = models.ForeignKey(
        "self",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="children"
    )
    name = models.CharField(max_length=15)
  • parent 없음 → 대분류
  • parent 있고, parent.parent 없음 → 중분류
  • parent 있고, parent.parent 있음 → 소분류
# 사용자 화면
[대분류 선택]  → 프론트엔드
[중분류 선택]  → 웹 프레임워크
[소분류 선택]  → Django

# 서버(최종 소분류 id 하나만 전달)
{
  "title": "...",
  "content": "...",
  "category": 12  // Django 카테고리 id
}

진행방식

  • 1. 카테고리는 “미리” 만들어져 있음 (관리자 영역)

idnameparent_id
1프론트엔드null
2백엔드null
3웹프레임워크1
4언어1
5Django3
6React3
# TREE
프론트엔드
 ├─ 웹프레임워크
 │   ├─ Django
 │   └─ React
 └─ 언어

# “질문 작성 화면”
## 사용자는 3번 클릭하지만 서버에는 1개의 값만 보냄
[대분류]  드롭다운  → 프론트엔드 / 백엔드
[중분류]  드롭다운  → (프론트엔드 선택 시) 웹프레임워크 / 언어
[소분류]  드롭다운  → (웹프레임워크 선택 시) Django / React

DB에 저장된 결과

Question 테이블

idtitlecategory_id
100Django 마이그레이션 질문5

QuestionCategory 테이블

idnameparent
5Django웹프레임워크

새롭게 알게된 내용 ✅


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


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

  • 문제: qna구조로 구조 전체를 변경하면서 코드를 분리했는데 테스트 코드가 전부 201을 뱉음
  • 원인
    • API 테스트가 기대하는 것
      • ✅ 성공 → 201 + { question_id }
      • ✅ 권한 실패 → 403
      • ✅ 제목 중복 → 409
      • ✅ 잘못된 카테고리 → 404
    • 수정한 API View 코드가 실제로 호출되지 않고 있다
    • 그래서 아무리 View를 고쳐도 DRF 기본 동작(CreateAPIView 스타일) 이 계속 실행되고,
    • 결과가 전부 201 + serializer.data로 나온다.
  • 해결
    • url이 specapi의 views를 가리키고 있었음
      • 수정후의 4개의 오류에서 1개로 감소
  • 문제 1. 400 - title / content / category 없는 경우
    • 400을 출력해야 하지만 404 - 존재하지 않는 카테고리 때문에
      • 카테고리가 없으면 무조건 404 출력
  • 해결
    • "category" 에러여도 ‘없는 값’이면 400, ‘존재하지 않는 id’면 404로 설정

profile
안녕하세요.

0개의 댓글