# 어제 무엇을 했나요?
- 1. 리뷰 조회 api 구현
- 2. "좋아요"기능 구현 (80%)
- 3. 각 메서드 별 에러메세지 분리
# 오늘은 무엇을 할 것인가요?
- 1. "좋아요"기능 구현(삭제 포함)
- 2. 리뷰 수정 api 구현
- 3. 리뷰 삭제 api 구현(가능하다면)
- 4. 프로젝트 스웨거 설정 변환
- yaml파일 / 내가 구현한 api 모두 스웨거에서 확인 가능하게 하기
# 진행하는데 어려운 부분(도움이 필요한 부분)이 있나요?
- 1. 스웨거 고민
# view
from rest_framework.views import APIView
from rest_framework.response import Response
from typing import cast
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from apps.user.models.user import User
from apps.community.services.review_like_service import create_review_like
class ReviewLikeAPIView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request: Request, review_id: int) -> Response:
user = cast(User, request.user)
is_liked, total_likes = create_review_like( # type: ignore
user=user, review_id=review_id
)
return Response(
{
"message": "성공적으로 반영되었습니다.",
"is_liked": is_liked, # True면 하트 채우기, False면 비우기
"like_count": total_likes, # 숫자를 이 값으로 덮어씌우기
},
status=status.HTTP_200_OK,
)
# service
from django.db import transaction
from apps.community.exceptions.review_exceptions import ReviewNotFound
from apps.community.models.reviews import Review
from apps.community.models.review_like import ReviewLike
from apps.user.models.user import User
@transaction.atomic
def create_review_like(user: User, review_id: int) -> tuple[bool, int]:
# 1. 리뷰 가져오기 (select_for_update로 동시성 제어 - 락 걸기)
try:
review = Review.objects.select_for_update().get(id=review_id) # type: ignore
except Review.DoesNotExist:
raise ReviewNotFound()
# 2. 중간 테이블(ReviewLike) 확인
like_obj, created = ReviewLike.objects.get_or_create(user=user, review=review) # type: ignore
if created:
# 없어서 새로 생성됨 -> 좋아요 등록
review.like_count += 1
is_liked = True
else:
# 이미 있어서 가져옴 -> 좋아요 취소
like_obj.delete() # 기록 삭제
review.like_count -= 1
is_liked = False
# 3. 변경된 like_count 저장
review.save()
# 4. 결과 반환 (현재 상태, 갱신된 총 개수)
return is_liked, review.like_count
# view
from typing import cast
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from apps.community.services.review_like_service import (
add_review_like,
remove_review_like,
)
from apps.user.models.user import User
class ReviewLikeAPIView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request: Request, review_id: int) -> Response:
user = cast(User, request.user)
total_likes = add_review_like(user=user, review_id=review_id)
return Response(
{
"message": "좋아요를 눌렀습니다.",
"is_liked": True,
"like_count": total_likes,
},
status=status.HTTP_201_CREATED,
)
def delete(self, request: Request, review_id: int) -> Response:
user = cast(User, request.user)
total_likes = remove_review_like(user=user, review_id=review_id)
return Response(
{
"message": "좋아요를 취소했습니다.",
"is_liked": False,
"like_count": total_likes,
},
status=status.HTTP_200_OK,
)
# service
from django.db import transaction
from apps.community.exceptions.review_exceptions import ReviewNotFound
from apps.community.models.reviews import Review
from apps.community.models.review_like import ReviewLike
from apps.user.models.user import User
@transaction.atomic
def add_review_like(user: User, review_id: int) -> int:
"""
좋아요 생성 (POST)
"""
try:
# 1. 리뷰 가져오기 (Lock)
review = Review.objects.select_for_update().get(id=review_id) # type: ignore
except Review.DoesNotExist:
raise ReviewNotFound()
# 2. 좋아요 생성 (get_or_create)
like_obj, created = ReviewLike.objects.get_or_create(user=user, review=review) # type: ignore
if created:
review.like_count += 1
review.save(update_fields=["like_count"]) # 필요한 컬럼만 수정
return review.like_count
@transaction.atomic
def remove_review_like(user: User, review_id: int) -> int:
"""
좋아요 삭제 (DELETE)
"""
try:
# 1. 리뷰 가져오기 (Lock)
review = Review.objects.select_for_update().get(id=review_id) # type: ignore
except Review.DoesNotExist:
raise ReviewNotFound()
# 2. 좋아요 삭제(deleted_count = 1(삭제)/0(유지))
deleted_count, _ = ReviewLike.objects.filter(user=user, review=review).delete() # type: ignore
if deleted_count > 0:
review.like_count -= 1
review.save(update_fields=["like_count"])
return review.like_count
현재방식은 toggle방식이라고 한다.
리뷰를 보면 RESTful 방식으로 전환해야 할 것 같다.
like_count 값이 증가함에 따라 한 row 전체가 수정되는 거였지만
review.save(update_fields=["like_count"])
RESTful 원칙 준수를 위해 POST(생성)와 DELETE(삭제)로 메서드를 명확히 분리
deleted_count, _ = ReviewLike.objects.filter(user=user, review=review).delete()
ReviewLike.objects.filter(user=user, review=review)delete()(1, {'community.ReviewLike': 1})[ 🔴 문제: No API definition provided 오류 ]
[ 🟡 원인: settings.py에 여러 스키마를 보여줄 urls 설정이 누락 / 단일 스키마만 보여주는 기본 설정과 충돌함. ]
[ 🔵 해결: settings.py의 SWAGGER_UI_SETTINGS 내부에 urls 리스트(기획서, 구현 코드 경로)를 추가하여 멀티 스키마 모드로 전환. ]
[ 🔴 문제: Reverse for 'None' not found ]
[ 🟡 원인: urls.py에서 SpectacularSwaggerView 호출 시 url_name 인자를 제거(None)하여, 뷰가 내부적으로 스키마 주소를 찾지 못함. ]
[ 🔵 해결: urls.py 설정을 원상복구하여 url_name="schema"를 명시적으로 전달함. (뷰가 정상적으로 로드되도록 수정) ]
[ 🔴 문제: No layout defined for "StandaloneLayout" ]
[ 🟡 원인: StandaloneLayout을 사용하려면 JavaScript presets 설정이 필요한데, 파이썬 딕셔너리 설정으로는 JS 변수를 전달하지 못함. ]
[ 🔵 해결: settings.py의 SWAGGER_UI_SETTINGS 값을 문자열("""...""") 형태로 변경하고, 그 안에 presets: [SwaggerUIStandalonePreset] 등 JS 코드를 직접 포함시킴 ]
[ 🔴 문제: 인증 방식 불일치 ]
[ 🟡 원인: Django REST_FRAMEWORK 설정에 기본 인증(Basic/Session)이 활성화되어 있고, JWT 설정이 반영되지 않음. ]
[ 🔵 해결: settings.py의 DEFAULT_AUTHENTICATION_CLASSES에서 Basic/Session을 제거하고, **JWTAuthentication**만 남기도록 수정. ]