UnionProject PlayType

김기훈·2026년 2월 8일
post-thumbnail

AI 🤖


AI Game Review Summary

구성 요소파일 위치역할
Viewai/views/review_summary_view.py클라이언트의 요약 요청을 받아 서비스를 호출
Serviceai/services/review_summary_service.py요약 생성 여부 판단, 리뷰 필터링, AI 호출 등 핵심 로직 수행
Signalai/signals/review_summary.py리뷰가 저장될 때마다 요약 갱신 조건을 체크하여 백그라운드 작업 실행
Taskai/tasks/review_summary.pyCelery를 이용한 비동기 백그라운드 요약 생성
Modelai/models/game_review_summary.py생성된 요약 데이터를 DB에 저장 (JSON 형태)
Utilsai/utils.py욕설 필터링 및 리뷰 유효성 검사

트리거 (Trigger)

  • API 호출

    • 유저가 게임 상세 페이지에 진입하거나 요약 버튼을 누를 때 GameReviewSummaryAPIView가 호출
    • 이때 get_summary(game_id)를 통해 요약 데이터를 요청합니다.
  • 리뷰 등록 시 자동 감지 (Signal)

    • trigger_ai_summary 시그널은 리뷰(Review)가 저장될 때마다 실행
    • 리뷰 개수가 10개 이상이고, 마지막 요약 생성일로부터 30일이 지났다면
      • 자동으로 run_ai_summary 태스크를 큐에 등록

서비스 로직 (ReviewSummaryService)

  • 갱신 조건 확인 ( _update_and_parse )

    • 기존 요약이 없거나
    • 마지막 업데이트로부터 30일(AI_SUMMARY_UPDATE_INTERVAL_DAYS)이 지났다면 갱신을 시도
  • 리뷰 필터링 (is_valid_review_for_ai)

    • ai/utils.py의 korcen 라이브러리를 사용하여 욕설을 감지
    • 단, 욕설이 있더라도 리뷰 길이가 길면 정보 가치가 있다고 판단하여 포함시킬 수 있는 로직이 들어있음
  • AI 프롬프트 구성

    • "20년 경력의 게임 전문 리뷰 분석가"라는 페르소나를 부여
    • 수집된 리뷰 텍스트를 JSON 스키마(GameSummary)에 맞춰 요약하도록 요청
  • 결과 저장

    • Gemini API로부터 받은 JSON 응답을 GameReviewSummary 모델의 text 필드에 저장하고 반환

AI User Tendency Analysis

  • 유저가 선택한 선호 장르와 태그 정보를 바탕으로, 해당 유저를 '한국어 10글자 이내'의 문구로 정의해주는 기능
구성 요소파일 위치역할
Viewai/views/user_tendency_view.py성향 분석 요청 API. 분석 중일 경우 '진행 중' 상태 반환
Serviceai/services/user_tendency_service.py캐시를 이용한 중복 요청 방지 및 AI 분석 로직 관리
Taskai/tasks/user_tendency.pyCelery를 이용한 비동기 성향 분석 실행
Modelai/models/user_tendency.py분석된 성향 문구를 유저와 1:1 매핑하여 저장

비동기 처리 및 캐싱 전략

  • get_or_create_tendency

    • DB 조회
      • 이미 분석된 데이터(ai_tendency)가 있다면 즉시 반환합니다.
    • 중복 방지 (Cache Lock)
      • 데이터가 없다면 분석을 해야 하는데
      • 동시에 여러 요청이 올 수 있으므로 캐시(tendencyanalysis_lock{user.id})를 확인
      • 이미 "processing" 상태라면 대기 메시지를 반환
    • 태스크 실행
      • 분석 중이 아니라면
      • 캐시에 락을 걸고 Celery 태스크(run_user_tendency_analysis)를 비동기로 호출합니다.

분석 실행

  • analyze_and_save

    • 데이터 준비
      • 유저의 선호 장르(Genres)와 태그(Tags)를 문자열로 변환합니다.
      • 데이터가 아예 없으면 분석을 수행하지 않고 기본값(None)을 저장합니다.
    • AI 프롬프트
      • "게임 심리학자"라는 페르소나를 부여하고,
      • 유저 정보를 바탕으로 10글자 이내의 한 줄 정의를 내리도록 Gemini에게 요청합니다.
      • 응답 포맷은 Pydantic 모델(UserTendency)을 사용하여 JSON 형식을 강제합니다.
    • 저장 및 해제
      • 분석 결과를 DB에 저장하고,
      • 태스크가 종료되면 finally 블록에서 캐시 락을 해제하여 다음 요청이 가능하도록 합니다.

캐시

서비스 레이어 (Service)

  • ai/services/user_tendency_service.py

    • 분석 요청이 들어왔을 때 캐시를 확인하여 중복 요청을 방지하고
    • 요청이 없다면 캐시에 '진행 중' 표시를 남기는 역할
    def get_or_create_tendency(self, user) -> dict:
        """
        API View에서 호출: DB 데이터를 우선 반환하고, 없으면 분석 요청
        """
        # 1. DB 데이터가 이미 있다면 바로 반환 (Persistent Cache)
        if hasattr(user, "ai_tendency"):
            return {"status": "completed", "tendency": user.ai_tendency.tendency}

        # ---------------------------------------------------------
        # [캐시 로직 시작] 중복 분석 요청 방지 (Cache Lock)
        # ---------------------------------------------------------
        
        # 유저별 고유 캐시 키 생성 (예: "tendency_analysis_lock_15")
        cache_key = f"tendency_analysis_lock_{user.id}"
        """
         - `f"tendency_analysis_lock_{user.id}"`
           - 유저 ID를 포함하여 유저별로 독립적인 락(Lock)을 관리합니다.
           - 즉, A 유저가 분석 중이어도 B 유저는 분석을 요청할 수 있습니다.
        """

        # 2. 캐시 조회: 이미 누군가(혹은 본인이) 요청해서 분석 중인지 확인
        if cache.get(cache_key):
            # 이미 Task가 돌고 있다면 API는 기다리라는 메시지만 반환
            """
            - `cache.get(cache_key)`가 존재하면 
              - `run_user_tendency_analysis.delay()`를 호출하지 않고 즉시 리턴
		    - 이는 사용자가 버튼을 연타하거나, 새로고침을 반복해도 AI API가 중복으로 호출되는 것을 막아줍니다.
            """
            return {
                "status": "processing",
                "message": "성향 분석이 진행 중입니다. 잠시만 기다려주세요.",
                "tendency": None,
            }

        # 3. 캐시 설정 (Locking): "지금부터 이 유저의 분석을 시작함" 표시
        # - value: "processing" (상태값)
        # - timeout: 60 * 5 (5분 뒤 자동 만료, 혹시 모를 데드락 방지)
        cache.set(cache_key, "processing", timeout=60 * 5)
        """
        - `timeout=60 * 5` (300초)
	    - 만약 분석 작업(Task)이 알 수 없는 에러로 죽어서 `cache.delete()`가 호출되지 않더라도,
          - 5분이 지나면 자동으로 락이 풀려 유저가 다시 요청할 수 있게 하는 안전장치입니다.
        """

        # ---------------------------------------------------------
        # [캐시 로직 끝]
        # ---------------------------------------------------------

        # 4. Celery 비동기 작업 호출
        run_user_tendency_analysis.delay(user.id)

        return {
            "status": "processing",
            "message": "성향 분석 요청이 접수되었습니다.",
            "tendency": None,
        }

비동기 태스크 (Task)

  • ai/tasks/user_tendency.py

    • 이 파일에서는 분석 작업이 완료(성공하든 실패하든)되면,
      • 다른 요청이 가능하도록 캐시를 삭제(Unlock) 하는 역할을 합니다
logger = logging.getLogger(__name__)

@shared_task
def run_user_tendency_analysis(user_id: int):
    
    # 서비스와 동일한 규칙으로 캐시 키 생성
    cache_key = f"tendency_analysis_lock_{user_id}"
    
    try:
        user = User.objects.get(id=user_id)
        
        # AI 분석 서비스 호출 (시간이 오래 걸리는 작업)
        service = UserTendencyService()
        service.analyze_and_save(user)
        
        logger.info(f"Successfully finished analysis for User ID: {user_id}")

    except Exception as e:
        logger.error(f"Error in User Tendency Task: {e}", exc_info=True)
    
    finally:
        # ---------------------------------------------------------
        # [캐시 로직] 락 해제 (Unlock)
        # ---------------------------------------------------------
        # 작업이 성공하든(try), 에러가 나든(except) 무조건 실행되어야 함
        cache.delete(cache_key)
        """
         - `cache.delete(cache_key)`
         - 서비스 레이어에서 걸어두었던 "processing" 상태를 제거합니다.
           - 이제 해당 유저는 다시 `get_or_create_tendency`를 호출했을 때
           - 캐시가 없으므로 새로운 요청을 하거나 DB 결과를 받을 수 있습니다.
         
         - finally 블록의 중요성
           - AI 분석 중 예외(API 오류, DB 오류 등)가 발생하더라도 락은 반드시 해제되어야 합니다.
           - `finally` 블록에 넣음으로써, 작업 실패 시 유저가 영원히 "분석 중" 상태에 갇히는 것(Deadlock)을 방지합니다.
        """

커뮤니티 📝


Review

기능API / Method주요 로직 및 특징관련 주요 파일
리뷰 등록ReviewAPIView (POST)- 유효성 검사: 평점(Rating)은 1~5점 사이의 정수만 허용 (Validators 적용)
- 관계 매핑: 작성자(User)와 대상 게임(Game)을 연결
- 예외 처리: 존재하지 않는 게임 ID 요청 시 에러 반환
models/reviews.py
review_api.py
review_create_service.py
리뷰 목록 조회ReviewAPIView (GET)- 페이지네이션: ReviewPageNumberPagination을 통한 목록 분할 전송
- 필터링: 특정 Game ID에 종속된 리뷰만 조회
review_api.py
review_list_service.py
내 리뷰 조회MyReviewListAPIView (GET)- 본인 확인: 로그인한 유저(request.user)가 작성한 리뷰만 필터링하여 조회review_api.py
review_list_service.py
좋아요 (투표)ReviewLikeAPIView- 중복 방지: 유저당 1개의 리뷰에 한 번만 좋아요 가능 (UniqueConstraint)
- 카운팅: Review 모델에 like_count 필드를 두어 집계 성능 최적화
models/review_like.py
review_like_service.py
데이터 관리Model Definition- Soft Delete: is_deleted 필드를 사용하여 실제 데이터 삭제 대신 플래그 처리 (데이터 보존)
- 인덱싱: 조회 성능 향상을 위한 DB Index 설정 (game, like_count)
models/reviews.py

comment

기능API / Method주요 로직 및 특징관련 주요 파일
댓글 등록ReviewCommentAPIView (POST)- 부모 검증: 댓글이 달릴 리뷰가 존재하는지, 삭제(is_deleted=True)되지는 않았는지 확인
- 작성자 매핑: 로그인한 유저 정보를 자동으로 작성자로 할당
models/comments.py
comment_api.py
comment_create_service.py
댓글 조회ReviewCommentAPIView (GET)- 통합 조회: 단순히 댓글만 가져오는 것이 아니라, 리뷰 상세 정보 + 해당 리뷰의 댓글 목록을 함께 반환하는 구조로 보임
- 정렬: 인덱스를 활용하여 작성일 순 등의 정렬 효율화
comment_api.py
comment_list_service.py
데이터 관리Model Definition- Soft Delete: 리뷰와 동일하게 is_deleted 필드를 통한 논리적 삭제 구현
- 관계: Review와 1:N 관계 (FK)
models/comments.py

profile
안녕하세요.

0개의 댓글