오늘 학습 내용 ✅
자주 사용하는 git 명령어 및 코드 리펙터 코드 정리
질문등록 api
- 변경된 프로젝트 구조에맞게 분리
- “View / Serializer / Service / Permission”
Permission
Servier
Serializer
View
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"})
error_type = e.detail.get("type")
if error_type == "title_conflict":
...
ValidationError({
"field_name": ["error message"]
})
ValidationError({
"non_field_errors": ["error message"]
})
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. 카테고리는 “미리” 만들어져 있음 (관리자 영역)
| id | name | parent_id |
|---|
| 1 | 프론트엔드 | null |
| 2 | 백엔드 | null |
| 3 | 웹프레임워크 | 1 |
| 4 | 언어 | 1 |
| 5 | Django | 3 |
| 6 | React | 3 |
# TREE
프론트엔드
├─ 웹프레임워크
│ ├─ Django
│ └─ React
└─ 언어
# “질문 작성 화면”
## 사용자는 3번 클릭하지만 서버에는 1개의 값만 보냄
[대분류] 드롭다운 → 프론트엔드 / 백엔드
[중분류] 드롭다운 → (프론트엔드 선택 시) 웹프레임워크 / 언어
[소분류] 드롭다운 → (웹프레임워크 선택 시) Django / React
DB에 저장된 결과
Question 테이블
| id | title | category_id |
|---|
| 100 | Django 마이그레이션 질문 | 5 |
QuestionCategory 테이블
| id | name | parent |
|---|
| 5 | Django | 웹프레임워크 |
새롭게 알게된 내용 ✅
어려운 내용(추가 학습 필요) ✅
오늘 발생한 문제(발생 했다면) ✅
- 문제: qna구조로 구조 전체를 변경하면서 코드를 분리했는데 테스트 코드가 전부 201을 뱉음
- 원인
- API 테스트가 기대하는 것
- ✅ 성공 → 201 + { question_id }
- ✅ 권한 실패 → 403
- ✅ 제목 중복 → 409
- ✅ 잘못된 카테고리 → 404
- 수정한 API View 코드가 실제로 호출되지 않고 있다
- 그래서 아무리 View를 고쳐도 DRF 기본 동작(CreateAPIView 스타일) 이 계속 실행되고,
- 결과가 전부 201 + serializer.data로 나온다.
- 해결
- url이 specapi의 views를 가리키고 있었음
- 문제 1. 400 - title / content / category 없는 경우
- 400을 출력해야 하지만 404 - 존재하지 않는 카테고리 때문에
- 해결
- "category" 에러여도 ‘없는 값’이면 400, ‘존재하지 않는 id’면 404로 설정