2026/01/15 합동프로젝트 - 7

김기훈·2026년 1월 15일

TIL

목록 보기
112/191
# 어제 무엇을 했나요?
- 1. 리뷰 조회 api 구현
- 2. "좋아요"기능 구현 (80%)
- 3. 각 메서드 별 에러메세지 분리

# 오늘은 무엇을 할 것인가요?
- 1. "좋아요"기능 구현(삭제 포함)
- 2. 리뷰 수정 api 구현 
- 3. 리뷰 삭제 api 구현(가능하다면)
- 4. 프로젝트 스웨거 설정 변환
  - yaml파일 / 내가 구현한 api 모두 스웨거에서 확인 가능하게 하기 

# 진행하는데 어려운 부분(도움이 필요한 부분)이 있나요?
- 1. 스웨거 고민

오늘 학습 내용 ✅


"like"

현재 코드

# 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

PR 리뷰

    1. 현재 like_count가 수정되었을때 review전체를 다시 저장함
    1. 동시 사용 ? post에서는 등록만 delete를 추가
  • 현재방식은 toggle방식이라고 한다.

    • post하나로 두 가지 상태를 모두 처리하는 것
  • 리뷰를 보면 RESTful 방식으로 전환해야 할 것 같다.


PR 리뷰 해결

  • like_count 값이 증가함에 따라 한 row 전체가 수정되는 거였지만

    • save() 호출 시 변경된 컬럼만 업데이트하도록 update_fields 옵션을 적용하기
  • review.save(update_fields=["like_count"])

    • Django의 save()는 기본적으로 모든 필드를 UPDATE
    • update_fields를 지정하면 딱 필요한 컬럼만 건드림
  • RESTful 원칙 준수를 위해 POST(생성)와 DELETE(삭제)로 메서드를 명확히 분리

  • deleted_count, _ = ReviewLike.objects.filter(user=user, review=review).delete()

    • 해당 유저가 해당 리뷰에 남긴 좋아요 기록을 찾아서, 그 즉시 삭제하고, 몇 개가 지워졌는지 알려달라
    • ReviewLike.objects
      • ReviewLike 테이블에 접근하기 위한 매니저
    • .filter(user=user, review=review)
      • 조건에 맞는 데이터 찾기
        • user: 요청을 보낸 유저 / review: 좋아요를 취소하려는 리뷰
    • delete()
      • delete() 메서드는 실행 후 항상 튜플(Tuple) 형태의 값을 반환
      • (총삭제개수, { '앱이름.모델명': 삭제_개수, ... })
      • ex
        • ReviewLike 데이터 1개가 삭제
        • (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**만 남기도록 수정. ]
profile
안녕하세요.

0개의 댓글