class QuestionListQuerySerializer(serializers.Serializer):
category = serializers.IntegerField(required=False)if category is not None:
qs = qs.filter(category_id=category)
class QuestionListSerializer(serializers.ModelSerializer):
category = serializers.CharField()
1. Question.category는 FK 객체
2. 응답이 기대 하는 내용 → "category": "프론트엔드 > React > 상태관리"
3. 즉, 조회조건에서는 ID / 응답에서는 문자열로 출력
QuestionCategory
├─ parent (self FK)
└─ children
1. 트리구조는 존재는 하지만 아직 사용은 X
2. 즉, “중분류를 선택하면 소분류 질문까지 같이 보여준다” → 아직 구현 안 됨
카테고리는 어드민에서만 관리한다
class Question(models.Model):
category = models.ForeignKey(
QuestionCategory,
on_delete=models.PROTECT
)
1. PROTECT를 쓰는 이유
- 이미 질문이 달린 카테고리는 삭제 ❌ / 데이터 무결성 보장 → 어드민 실수 방지
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)
1. 모델의 의미
카테고리는 트리 구조 / 깊이는 고정 ❌ (2단계든 3단계든 가능) / “대/중/소”는 UI 개념, DB 제약 X
class Question(models.Model):
category = models.ForeignKey(
QuestionCategory,
on_delete=models.PROTECT,
)
2. Question은 카테고리 하나만 참조한다
{
"main_category": 1,
"sub_category": 3,
"detail_category": 7
}
- 이 방식을 사용하면, DB 모델과 안 맞음 / 불필요한 중복 정보 / 정합성 검증 지옥
- “대분류와 중분류가 실제로 연결돼 있나?” / “중분류 없이 소분류가 가능한가?” / 테스트 폭발
{
"title": "질문 제목",
"content": "질문 내용",
"category": 7
}
- 7이 대분류일 가능성 / 중분류일 가능성 / 소분류일 가능성 백엔드는 여기있는 ID 하나만 신뢰
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 == None → 루트 카테고리 (UI상 대분류)
parent != None
├─ children 있음 → 중간 노드 (UI상 중분류)
└─ children 없음 → leaf 노드 (UI상 소분류)
| UI 개념 | 백엔드 판단 기준 |
|---|---|
| 대분류 | parent is None |
| 중분류 | parent != None AND children.exists() |
| 소분류 | children.exists() == False |
def get_descendant_ids(category):
ids = [category.id]
for child in category.children.all():
ids.extend(get_descendant_ids(child))
return ids
# 조회 방법
category_ids = get_descendant_ids(selected_category)
qs = qs.filter(category_id__in=category_ids)
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)
question_categories
-------------------
id | name | parent_id
1. parent_id 는 같은 question_categories.id 를 가리킴
| id | name | parent_id | 백엔드 (id=1) # 백엔드.parent_id = NULL
| -- | ------ | --------- | └─ Django (id=2) # Django.parent_id = 1
| 1 | 백엔드 | NULL | └─ ORM (id=3) # ORM.parent_id = 2
| 2 | Django | 1 |
| 3 | ORM | 2 |
# 자식 → 부모
orm = QuestionCategory.objects.get(name="ORM")
orm.parent # Django 카테고리 객체
# 부모 → 자식
django = QuestionCategory.objects.get(name="Django")
django.children.all() # ORM 카테고리 queryset
if category.parent is None:
# 루트
elif category.children.exists():
# 중간 노드
else:
# leaf
backend = QuestionCategory.objects.create(name="백엔드")
django = QuestionCategory.objects.create(name="Django", parent=backend)
orm = QuestionCategory.objects.create(name="ORM", parent=django)
# Django 내부적으로 하는 일
django.parent_id = backend.id
orm.parent_id = django.id
backend = QuestionCategory.objects.create(name="백엔드")
| id | name | parent_id |
|---|---|---|
| 1 | 백엔드 | NULL |
parent=backend 는 객체를 넘긴 것
Django ORM 내부에서는
| id | name | parent_id |
|---|---|---|
| 1 | 백엔드 | NULL |
| 2 | Django | 1 |
backend = QuestionCategory(name="백엔드")
django = QuestionCategory.objects.create(name="Django", parent=backend)
- 여기서는 에러 발생
- backend는 아직 DB에 저장되지 않아서 backend.id == None
- QuestionCategory.objects.create(name="백엔드") 이렇게 사용 필요
# 필드 하나 누락 = 문자열
raise ValidationError("잘못된 요청") → "detail": "잘못된 요청"
# 필드 단위 에러 (serializer 기본) = dict + list
{
"title": ["This field is required."],
"content": ["This field may not be blank."]
}
exc.detail == {
"title": ["This field is required."],
"content": ["This field may not be blank."]
}
# non_field_errors = dict + list
{
"non_field_errors": ["중복된 값입니다."]
}
# ListSerializer / bulk validation = list of dict
[
{"title": ["required"]},
{"content": ["required"]}
]
if isinstance(exc, ValidationError):
detail = exc.detail
if isinstance(detail, list) and detail: # list
response.data = {"error_detail": str(detail[0])}
elif isinstance(detail, dict): # dict
response.data = {"error_detail": next(iter(detail.values()))}
else: # 나머지
response.data = {"error_detail": str(detail)}
return response
if (
isinstance(exc, ValidationError)
and isinstance(view, QuestionCreateAPIView)
):
response.data = {
"error_detail": "유효하지 않은 질문 등록 요청입니다."
}
return responsecategory = get_category_or_raise(serializer.validated_data["category"])
question = create_question(
author=user,
title=serializer.validated_data["title"],
content=serializer.validated_data["content"],
category=category,
image_urls=serializer.validated_data.get("image_urls", []),
)
1. serializer.validated_data에서 title / content / image_urls 를 하나씩 꺼내서 다시 함수 인자로 나열
2. validated_data가 이미 “질문 생성에 필요한 데이터 묶음”인데 View에서 다시 풀어헤치고 있다는 느낌
3. 즉, validated_data로 대체 가능 이라는 말은
3-1. 이미 검증된 데이터 묶음이 있으니까 그걸 그대로 service로 넘기면 되지 않은가? 라는 말