Skip to content

Merge to main#16

Merged
H4nnhoi merged 4 commits intomainfrom
develop
Nov 3, 2025
Merged

Merge to main#16
H4nnhoi merged 4 commits intomainfrom
develop

Conversation

@H4nnhoi
Copy link
Contributor

@H4nnhoi H4nnhoi commented Nov 3, 2025

🔎 Description

write what you do

🔗 Related Issue

🏷️ What type of PR is this?

  • ✨ Feature
  • ♻️ Code Refactor
  • 🐛 Bug Fix
  • 🚑 Hot Fix
  • 📝 Documentation Update
  • 🎨 Style
  • ⚡️ Performance Improvements
  • ✅ Test
  • 👷 CI
  • 💚 CI Fix

📋 Changes Made

  • Change 1
  • Change 2
  • Change 3

🧪 Testing

  • Unit tests pass
  • Integration tests pass
  • Manual testing completed

📸 Screenshots (if applicable)

📝 Additional Notes

Summary by CodeRabbit

  • 새로운 기능

    • AI 기반 주간/월간 감정 분석 API 및 응답 구조 추가
    • FCM 단일 토큰 테스트 전송 지원
  • 개선사항

    • 음성 처리 안정성·타임아웃·오류 복구 강화
    • STT/NLP 백그라운드 처리의 시간제한 및 로깅 개선
  • 데이터베이스

    • 분석 결과 저장용 캐시 테이블 추가(주간/빈도)
  • 문서

    • Glances 사용 가이드(한국어) 추가

@coderabbitai
Copy link

coderabbitai bot commented Nov 3, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

OpenAI 기반 음성 감정 분석 서비스와 캐싱 모델(weekly_result, frequency_result)을 추가하고, API 엔드포인트 응답 모델을 갱신했으며 오디오 변환·STT 타임아웃·백그라운드 처리를 개선하고 관련 문서를 하나 추가했습니다.

Changes

Cohort / File(s) 요약
문서
GLANCES_USAGE.md
Glances 사용 가이드 한글 문서 추가 (설치, 사용법, 서비스 관리 등).
DTO 정의
app/dto.py
분석 응답용 DTO 4개 추가: AnalysisResultResponse, WeeklyDayItem, WeeklyAnalysisCombinedResponse, FrequencyAnalysisCombinedResponse.
API 라우트 / 엔트리포인트
app/main.py
분석 엔드포인트에 새로운 response_model 적용 및 analysis_service 통합; 테스트 FCM 전송 라우트 추가; 에러 처리(HTTP 400) 추가.
ORM / 스키마
app/models.py, database_schema.sql, migrations/versions/202511010003_add_weekly_frequency_result.py, migrations/versions/202511030001_merge_heads.py
weekly_resultfrequency_result 테이블용 ORM 모델·SQL·Alembic 마이그레이션 추가(인덱스·유니크 제약 포함).
분석 서비스
app/services/analysis_service.py
OpenAI 클라이언트 래퍼, 주간·주파수 쿼리·프롬프트 빌더, 결과 캐싱·갱신 로직 및 public API (get_weekly_result, get_frequency_result) 추가.
STT 및 음성 처리 개선
app/stt_service.py, app/voice_service.py
STT 타임아웃 파라미터 추가(전파), ffmpeg 파일 입출력 기반 변환 경로 도입 및 WAV 유효성 검사, 비동기 STT·NLP에 전체 20초 데드라인·타임아웃 처리, 오류·정리 로직 강화.
FCM 서비스
app/services/fcm_service.py
토큰 목록을 직접 받아 전송하는 send_notification_to_tokens 메서드 추가.
의존성
requirements.txt
openai>=1.40.0 패키지 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Client as 클라이언트
    participant API as FastAPI 엔드포인트
    participant Analysis as analysis_service
    participant DB as DB 캐시 (Weekly/Frequency)
    participant OpenAI as OpenAI API

    Client->>API: GET /voices/analyzing/weekly
    API->>Analysis: get_weekly_result(session, username)
    Analysis->>DB: 최신 weekly_result 조회 (user)
    alt 캐시 존재 (latest_voice_composite_id 동일)
        DB-->>Analysis: cached message
        Analysis-->>API: WeeklyAnalysisCombinedResponse(합침)
    else 캐시 미스
        Analysis->>Analysis: _query_weekly_top_emotions() (DB에서 음성 집계)
        Analysis->>Analysis: _build_weekly_prompt()
        Analysis->>OpenAI: chat completion 요청
        OpenAI-->>Analysis: 분석 메시지
        Analysis->>DB: WeeklyResult 저장/갱신
        Analysis-->>API: WeeklyAnalysisCombinedResponse(합침)
    end
    API-->>Client: 응답 반환
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • 집중 검토 대상:
    • app/services/analysis_service.py (OpenAI 호출, 프롬프트·쿼리 로직, 트랜잭션/캐싱)
    • app/voice_service.py (ffmpeg 변환 경로, 파일 정합성 검사, async 타임아웃/예외 흐름)
    • 마이그레이션 및 모델 정의 (제약·인덱스·외래키 일관성)
    • 엔드포인트 변경에 따른 응답 모델 호환성 검사

Possibly related PRs

  • Merge to main #11 — 주간/주파수 분석 엔드포인트 및 유사한 분석 흐름을 다루는 PR로, DTO·엔드포인트 변경과 밀접 관련.
  • hotfix #13 — STT/음성 처리 및 분석 관련 핵심 파일들을 동시에 수정한 PR로 코드 수준의 충돌 가능성 있음.
  • Merge to main #14 — FCM 서비스 변경(토큰 전송 관련)과 중복되는 변경점을 포함할 가능성이 있음.

Poem

🐰 바쁜 발로 데이터를 모아,
하루와 달의 감정들을 세어,
OpenAI에게 속삭여 묻고,
캐시에 담아 조용히 쉬네,
토끼가 응원해요, 잘 했어요 🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목인 'Merge to main'은 변경 사항의 주요 내용과 관련이 없습니다. 실제 PR에는 OpenAI 분석 기능 추가, 데이터베이스 스키마 확장, STT 타임아웃 기능 등 여러 중요한 변경 사항이 포함되어 있지만, 제목에는 이러한 구체적인 정보가 전혀 반영되지 않았습니다. 제목은 단순히 브랜치 병합 작업을 설명할 뿐 실제 비즈니스 가치나 기술적 변화를 전달하지 못합니다. PR 제목을 변경하여 주요 변경 사항을 명확하게 반영하도록 수정하세요. 예를 들어, 'Add OpenAI-powered analysis service with weekly and frequency endpoints' 또는 'Integrate OpenAI analysis with caching layer for voice data' 등으로 변경하면 팀원들이 히스토리에서 PR의 핵심 목적을 빠르게 파악할 수 있습니다.
Description check ⚠️ Warning PR 설명이 템플릿의 필수 섹션 대부분을 비워두고 있습니다. '🔎 Description' 섹션은 '> write what you do'라는 플레이홀더 텍스트만 남겨져 있고, '🔗 Related Issue'는 공백이며, '📋 Changes Made' 섹션의 세 개 항목 모두 미완성 상태입니다. '🧪 Testing' 섹션의 테스트 항목들도 모두 미체크 상태이며, 실제 변경 사항에 대한 구체적인 설명이 전혀 없습니다. PR 설명을 완성하세요. '🔎 Description'에는 이 PR이 OpenAI 분석 서비스 추가, 주간/빈도 분석 엔드포인트 구현, 캐싱 메커니즘 도입 등의 내용을 작성하고, '📋 Changes Made'에는 구체적인 변경 사항들(예: 새 DTO 추가, analysis_service 구현, 데이터베이스 테이블 추가 등)을 나열하며, '🧪 Testing' 섹션에서 수행한 테스트 항목을 체크하고, 필요한 경우 '📝 Additional Notes'에 추가 정보를 기록하세요.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 360e17f and cc68ea1.

📒 Files selected for processing (3)
  • app/main.py (4 hunks)
  • app/services/analysis_service.py (1 hunks)
  • app/services/fcm_service.py (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@H4nnhoi H4nnhoi merged commit 3a44545 into main Nov 3, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (4)
app/services/analysis_service.py (2)

75-75: 사용하지 않는 변수 제거

루프 변수 v가 사용되지 않습니다.

Based on static analysis

-    for v, vc in q:
+    for _, vc in q:
         em = (vc.top_emotion or "unknown") if vc else "unknown"
         cnt[em] += 1

136-145: 예외 메시지를 별도 상수로 정의

여러 곳에서 반복되는 문자열 예외 메시지를 별도 상수로 정의하면 유지보수가 용이합니다.

Based on static analysis

파일 상단에 상수 정의:

# 에러 메시지 상수
ERR_USER_NOT_FOUND = "user not found"
ERR_INVALID_CARE_USER = "invalid care user or not connected"
ERR_CONNECTED_USER_NOT_FOUND = "connected user not found"

사용 예:

if not owner:
    raise ValueError(ERR_USER_NOT_FOUND)
app/main.py (2)

365-376: 예외 체이닝 누락

예외를 다시 발생시킬 때 원본 예외 컨텍스트가 손실됩니다.

Based on static analysis

     except ValueError as e:
-        raise HTTPException(status_code=400, detail=f"잘못된 요청: {str(e)}")
+        raise HTTPException(status_code=400, detail=f"잘못된 요청: {str(e)}") from e
     except Exception as e:
-        raise HTTPException(status_code=500, detail=f"서버 오류: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"서버 오류: {str(e)}") from e

365-390: 분석 실패 시 부분 결과 반환 고려

OpenAI API 호출 실패 시에도 기존 빈도/주간 데이터는 여전히 유용할 수 있습니다. 현재는 전체 요청이 실패하지만, 메시지 필드를 optional로 만들어 부분 결과를 반환하는 것을 고려해보세요.

예시:

try:
    message = get_frequency_result(db, username=username, is_care=False)
except Exception as e:
    # OpenAI 실패 시에도 기존 데이터는 제공
    logger.warning(f"OpenAI analysis failed: {e}")
    message = "분석 생성 중 오류가 발생했습니다."

voice_service = get_voice_service(db)
base = voice_service.get_user_emotion_monthly_frequency(username, month)
frequency = base.get("frequency", {}) if base.get("success") else {}
return FrequencyAnalysisCombinedResponse(message=message, frequency=frequency)

이 경우 DTO에서 message 필드를 Optional로 변경하거나 에러 메시지를 반환하는 방식으로 처리할 수 있습니다.

Also applies to: 544-583

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a760621 and 360e17f.

📒 Files selected for processing (11)
  • GLANCES_USAGE.md (1 hunks)
  • app/dto.py (1 hunks)
  • app/main.py (3 hunks)
  • app/models.py (1 hunks)
  • app/services/analysis_service.py (1 hunks)
  • app/stt_service.py (3 hunks)
  • app/voice_service.py (4 hunks)
  • database_schema.sql (1 hunks)
  • migrations/versions/202511010003_add_weekly_frequency_result.py (1 hunks)
  • migrations/versions/202511030001_merge_heads.py (1 hunks)
  • requirements.txt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
migrations/versions/202511030001_merge_heads.py (1)
migrations/versions/202511010003_add_weekly_frequency_result.py (2)
  • upgrade (18-43)
  • downgrade (46-53)
app/main.py (5)
app/dto.py (3)
  • AnalysisResultResponse (245-248)
  • WeeklyAnalysisCombinedResponse (257-260)
  • FrequencyAnalysisCombinedResponse (263-266)
app/database.py (1)
  • get_db (38-44)
app/services/analysis_service.py (2)
  • get_frequency_result (180-226)
  • get_weekly_result (130-177)
app/voice_service.py (3)
  • get_voice_service (864-866)
  • get_user_emotion_monthly_frequency (774-799)
  • get_user_emotion_weekly_summary (801-861)
app/care_service.py (3)
  • get_emotion_monthly_frequency (12-45)
  • CareService (7-119)
  • get_emotion_weekly_summary (47-119)
app/voice_service.py (4)
app/stt_service.py (1)
  • transcribe_voice (145-147)
app/performance_logger.py (1)
  • log_step (22-37)
app/repositories/job_repo.py (2)
  • mark_text_done (98-101)
  • try_aggregate (110-160)
app/nlp_service.py (1)
  • analyze_text_sentiment (202-204)
app/services/analysis_service.py (2)
app/models.py (5)
  • Voice (32-58)
  • VoiceComposite (146-180)
  • User (8-29)
  • WeeklyResult (242-257)
  • FrequencyResult (260-275)
app/auth_service.py (1)
  • get_auth_service (297-299)
🪛 Ruff (0.14.3)
app/main.py

366-366: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


375-375: Do not catch blind exception: Exception

(BLE001)


376-376: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


376-376: Use explicit conversion flag

Replace with conversion flag

(RUF010)


379-379: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


389-389: Do not catch blind exception: Exception

(BLE001)


390-390: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


390-390: Use explicit conversion flag

Replace with conversion flag

(RUF010)


546-546: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


557-557: Do not catch blind exception: Exception

(BLE001)


558-558: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


558-558: Use explicit conversion flag

Replace with conversion flag

(RUF010)


571-571: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


582-582: Do not catch blind exception: Exception

(BLE001)


583-583: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


583-583: Use explicit conversion flag

Replace with conversion flag

(RUF010)

app/voice_service.py

86-86: subprocess call: check for execution of untrusted input

(S603)


99-99: Do not catch blind exception: Exception

(BLE001)


110-110: Do not catch blind exception: Exception

(BLE001)


117-118: try-except-pass detected, consider logging the exception

(S110)


117-117: Do not catch blind exception: Exception

(BLE001)


369-370: try-except-pass detected, consider logging the exception

(S110)


369-369: Do not catch blind exception: Exception

(BLE001)

app/services/analysis_service.py

14-14: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Loop control variable v not used within loop body

Rename unused v to _v

(B007)


136-136: Avoid specifying long messages outside the exception class

(TRY003)


142-142: Avoid specifying long messages outside the exception class

(TRY003)


145-145: Avoid specifying long messages outside the exception class

(TRY003)


186-186: Avoid specifying long messages outside the exception class

(TRY003)


191-191: Avoid specifying long messages outside the exception class

(TRY003)


194-194: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (6)
app/models.py (2)

242-257: 캐싱 모델 구조가 적절합니다

주간 분석 결과를 캐싱하는 모델 설계가 잘 되어 있습니다:

  • user_id에 대한 unique constraint로 사용자당 하나의 캐시만 유지
  • latest_voice_composite_id를 통한 캐시 무효화 전략이 명확함
  • 인덱스 구성이 조회 패턴에 적합함

260-275: FrequencyResult 모델이 WeeklyResult와 일관성 있게 구현되었습니다

월간 빈도 분석 캐싱 모델이 주간 분석 모델과 동일한 패턴으로 구현되어 일관성이 좋습니다.

app/services/analysis_service.py (3)

9-15: OpenAI API 키 검증이 적절합니다

환경 변수에서 API 키를 로드하고 누락 시 명확한 에러를 발생시키는 로직이 좋습니다.


31-52: 날짜 범위 계산 검증 필요

최근 7일 조회 시 start_dt = end_dt - timedelta(days=6)로 계산하고 있는데, 이는 오늘 포함 7일이 맞는지 확인이 필요합니다. 예를 들어 오늘이 11월 3일이면 10월 28일부터 11월 3일까지 7일이 조회됩니다.

비즈니스 요구사항이 "지난 7일" (오늘 제외)인 경우 다음과 같이 수정해야 합니다:

start_dt = end_dt - timedelta(days=7)
# 그리고 end_dt를 어제로 설정
end_dt = end_dt - timedelta(days=1)

18-28: 제안된 타임아웃 구현 방식이 올바르지 않습니다

OpenAI Python 클라이언트에서 타임아웃은 create() 메서드의 직접 파라미터로 전달되지 않습니다. 타임아웃은 클라이언트 수준에서 설정하거나 with_options()를 통해 설정해야 합니다.

올바른 구현은 _get_openai_client() 함수에서 타임아웃을 설정하는 것입니다:

def _get_openai_client():
    """OpenAI 클라이언트 생성 (env에서 키 로드)"""
    from openai import OpenAI
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        raise RuntimeError("OPENAI_API_KEY not configured in environment")
    return OpenAI(api_key=api_key, timeout=30.0)

또는 호출 시점에 with_options()을 사용할 수도 있습니다.

Likely an incorrect or invalid review comment.

app/main.py (1)

30-31: 새로운 DTO 임포트가 명확합니다

분석 결과를 위한 새로운 응답 모델들이 적절히 임포트되었습니다.

Comment on lines +365 to +376
@users_router.get("/voices/analyzing/frequency", response_model=FrequencyAnalysisCombinedResponse)
async def get_user_emotion_frequency(username: str, month: str, db: Session = Depends(get_db)):
"""사용자 본인의 한달간 감정 빈도수 집계"""
voice_service = get_voice_service(db)
result = voice_service.get_user_emotion_monthly_frequency(username, month)
if not result.get("success"):
raise HTTPException(status_code=400, detail=result.get("message", "조회 실패"))
return result
"""사용자 본인의 월간 빈도 종합분석(OpenAI 캐시 + 기존 빈도 결과)"""
from .services.analysis_service import get_frequency_result
try:
message = get_frequency_result(db, username=username, is_care=False)
voice_service = get_voice_service(db)
base = voice_service.get_user_emotion_monthly_frequency(username, month)
frequency = base.get("frequency", {}) if base.get("success") else {}
return FrequencyAnalysisCombinedResponse(message=message, frequency=frequency)
except Exception as e:
raise HTTPException(status_code=400, detail=f"분석 실패: {str(e)}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

모든 예외를 400으로 처리하는 것은 부적절합니다

OpenAI API 타임아웃, 데이터베이스 연결 오류 등 서버 측 문제는 500 에러로 처리되어야 합니다. 현재는 모든 예외를 400(클라이언트 오류)으로 반환하여 클라이언트가 재시도 로직을 올바르게 구현하기 어렵습니다.

예외 유형에 따라 적절한 상태 코드를 반환하도록 수정하세요:

-    except Exception as e:
-        raise HTTPException(status_code=400, detail=f"분석 실패: {str(e)}")
+    except ValueError as e:
+        # 사용자 입력 오류 (user not found 등)
+        raise HTTPException(status_code=400, detail=f"잘못된 요청: {str(e)}")
+    except Exception as e:
+        # 서버 내부 오류 (OpenAI API 오류, DB 오류 등)
+        raise HTTPException(status_code=500, detail=f"서버 오류: {str(e)}")
🧰 Tools
🪛 Ruff (0.14.3)

366-366: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


375-375: Do not catch blind exception: Exception

(BLE001)


376-376: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


376-376: Use explicit conversion flag

Replace with conversion flag

(RUF010)

Comment on lines +378 to +390
@users_router.get("/voices/analyzing/weekly", response_model=WeeklyAnalysisCombinedResponse)
async def get_user_emotion_weekly(username: str, month: str, week: int, db: Session = Depends(get_db)):
"""사용자 본인의 월/주차별 요일별 top 감정 요약"""
voice_service = get_voice_service(db)
result = voice_service.get_user_emotion_weekly_summary(username, month, week)
"""사용자 본인의 주간 종합분석(OpenAI 캐시 사용)"""
from .services.analysis_service import get_weekly_result
try:
message = get_weekly_result(db, username=username, is_care=False)
# 기존 주간 요약도 함께 제공
voice_service = get_voice_service(db)
weekly_result = voice_service.get_user_emotion_weekly_summary(username, month, week)
weekly = weekly_result.get("weekly", []) if weekly_result.get("success") else []
return WeeklyAnalysisCombinedResponse(message=message, weekly=weekly)
except Exception as e:
raise HTTPException(status_code=400, detail=f"분석 실패: {str(e)}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

get_user_emotion_weekly 엔드포인트에 동일한 예외 처리 이슈

Line 365-376에서 지적한 예외 처리 문제가 이 엔드포인트에도 동일하게 존재합니다.

동일한 수정사항을 적용하세요.

🧰 Tools
🪛 Ruff (0.14.3)

379-379: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


389-389: Do not catch blind exception: Exception

(BLE001)


390-390: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


390-390: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In app/main.py around lines 378-390, the endpoint uses a broad "except Exception
as e" like the earlier block; replace that with targeted error handling by
catching only expected exceptions (or rethrowing unexpected ones), and when
handling errors log the full stacktrace via the application's logger (e.g.,
process_logger.exception) before raising an HTTPException; ensure the
HTTPException returns an appropriate status code (500 for unexpected server
errors or a more specific code for known errors) and a concise detail message
instead of exposing internal stack traces.

Comment on lines +544 to +558
@care_router.get("/users/voices/analyzing/frequency", response_model=FrequencyAnalysisCombinedResponse)
async def get_emotion_monthly_frequency(
care_username: str, month: str, db: Session = Depends(get_db)
):
"""
보호자 페이지: 연결된 유저의 한달간 감정 빈도수 집계 (CareService 내부 로직 사용)
"""
care_service = CareService(db)
return care_service.get_emotion_monthly_frequency(care_username, month)
"""보호자: 연결 유저의 월간 빈도 종합분석(OpenAI 캐시 + 기존 빈도 결과)"""
from .services.analysis_service import get_frequency_result
try:
message = get_frequency_result(db, username=care_username, is_care=True)
from .care_service import CareService
care_service = CareService(db)
base = care_service.get_emotion_monthly_frequency(care_username, month)
frequency = base.get("frequency", {}) if base.get("success") else {}
return FrequencyAnalysisCombinedResponse(message=message, frequency=frequency)
except Exception as e:
raise HTTPException(status_code=400, detail=f"분석 실패: {str(e)}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Care 엔드포인트에도 동일한 예외 처리 이슈

예외 유형 구분 및 예외 체이닝 문제가 동일하게 존재합니다.

Lines 365-376에서 제안한 수정사항을 이 엔드포인트에도 적용하세요.

🧰 Tools
🪛 Ruff (0.14.3)

546-546: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


557-557: Do not catch blind exception: Exception

(BLE001)


558-558: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


558-558: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In app/main.py around lines 544-558, replace the broad "except Exception as e"
with targeted exception handling: let existing HTTPException pass through
(except HTTPException: raise), catch expected domain/validation/db errors (e.g.,
ValueError, sqlalchemy.exc.SQLAlchemyError, or your service-specific exceptions)
and convert them to an HTTPException using "raise HTTPException(status_code=400,
detail=f'분석 실패: {e}') from e" to preserve exception chaining; for any other
unexpected exceptions you can log them and raise a generic HTTPException with
chaining as well. Ensure you import any exception classes you reference (e.g.,
SQLAlchemyError) and use "from e" when re-raising to keep the original
traceback.

Comment on lines +566 to +583
@care_router.get("/users/voices/analyzing/weekly", response_model=WeeklyAnalysisCombinedResponse)
async def get_emotion_weekly_summary(
care_username: str,
month: str,
week: int,
db: Session = Depends(get_db)
):
"""보호자페이지 - 연결유저 월/주차별 요일 top 감정 통계"""
care_service = CareService(db)
return care_service.get_emotion_weekly_summary(care_username, month, week)
"""보호자: 연결 유저의 주간 종합분석(OpenAI 캐시 사용)"""
from .services.analysis_service import get_weekly_result
try:
message = get_weekly_result(db, username=care_username, is_care=True)
# 기존 주간 요약도 함께 제공
care_service = CareService(db)
weekly_result = care_service.get_emotion_weekly_summary(care_username, month, week)
weekly = weekly_result.get("weekly", []) if weekly_result.get("success") else []
return WeeklyAnalysisCombinedResponse(message=message, weekly=weekly)
except Exception as e:
raise HTTPException(status_code=400, detail=f"분석 실패: {str(e)}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Care weekly 엔드포인트에도 동일한 예외 처리 이슈

모든 분석 엔드포인트에서 일관되게 예외 처리를 개선해야 합니다.

Lines 365-376에서 제안한 예외 처리 패턴을 이 엔드포인트에도 적용하세요.

🧰 Tools
🪛 Ruff (0.14.3)

571-571: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


582-582: Do not catch blind exception: Exception

(BLE001)


583-583: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


583-583: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In app/main.py around lines 566 to 583, the endpoint uses a broad except that
returns a 400 with only the exception string; apply the same improved
exception-handling pattern used at lines 365-376: replace the broad except with
targeted handling (catch expected service/validation errors and raise
HTTPException with appropriate status and user-facing detail), and for any
unexpected Exception call logger.exception(...) to record the full stack trace
and then raise HTTPException(status_code=500, detail="서버 오류가 발생했습니다") so
internal errors are logged but not leaked to clients.

Comment on lines +130 to +177
def get_weekly_result(session: Session, username: str, is_care: bool = False) -> str:
"""주간 종합분석 결과 메시지 생성"""
from ..auth_service import get_auth_service
auth = get_auth_service(session)
owner = auth.get_user_by_username(username)
if not owner:
raise ValueError("user not found")

# care인 경우 연결된 유저로 전환
target_user = owner
if is_care:
if owner.role != 'CARE' or not owner.connecting_user_code:
raise ValueError("invalid care user or not connected")
target_user = auth.get_user_by_username(owner.connecting_user_code)
if not target_user:
raise ValueError("connected user not found")

# 최신 voice_composite_id 조회
latest_vc = (
session.query(VoiceComposite.voice_composite_id)
.join(Voice, Voice.voice_id == VoiceComposite.voice_id)
.filter(Voice.user_id == target_user.user_id)
.order_by(VoiceComposite.created_at.desc())
.first()
)
latest_vc_id = latest_vc[0] if latest_vc else None

# 캐시 조회
cache = session.query(WeeklyResult).filter(WeeklyResult.user_id == target_user.user_id).first()
if cache and cache.latest_voice_composite_id == latest_vc_id:
return cache.message

# 생성 후 캐시 저장/갱신
by_day = _query_weekly_top_emotions(session, target_user.user_id)
messages = _build_weekly_prompt(target_user.name, by_day)
msg = _call_openai(messages)

if cache:
cache.message = msg
cache.latest_voice_composite_id = latest_vc_id
else:
session.add(WeeklyResult(
user_id=target_user.user_id,
latest_voice_composite_id=latest_vc_id,
message=msg,
))
session.commit()
return msg
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

캐시 무효화 로직에 잠재적 경쟁 조건 존재

여러 요청이 동시에 들어올 경우 같은 사용자에 대해 중복으로 OpenAI API를 호출하고 캐시를 업데이트할 수 있습니다. 캐시 확인(line 158-160)과 OpenAI 호출(line 165) 사이에 다른 요청이 캐시를 업데이트할 수 있습니다.

다음 해결 방안을 고려하세요:

  1. 데이터베이스 레벨 락 사용 (SELECT FOR UPDATE)
  2. Redis 등 분산 락 사용
  3. 중복 호출을 허용하되 마지막 업데이트만 반영 (현재 방식 유지하되 문서화)

옵션 1 (SELECT FOR UPDATE) 예시:

# 캐시 조회 시 락 획득
cache = session.query(WeeklyResult).filter(
    WeeklyResult.user_id == target_user.user_id
).with_for_update().first()

참고: 이 방식은 트랜잭션 격리 수준과 데드락 가능성을 고려해야 합니다.

🧰 Tools
🪛 Ruff (0.14.3)

136-136: Avoid specifying long messages outside the exception class

(TRY003)


142-142: Avoid specifying long messages outside the exception class

(TRY003)


145-145: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In app/services/analysis_service.py around lines 130-177, the cache-validation
-> OpenAI call has a race where concurrent requests can both call OpenAI and
overwrite the cache; fix by acquiring a lock before deciding to call OpenAI:
begin a DB transaction, re-query the WeeklyResult for target_user.user_id using
SELECT ... FOR UPDATE (SQLAlchemy: with_for_update()) to lock the row (or use a
distributed lock like Redis if preferred), re-check latest_voice_composite_id
against latest_vc_id inside the lock/transaction, only call OpenAI if the cached
message is still stale, then update or insert the WeeklyResult and commit the
transaction; be mindful of transaction boundaries/timeout and potential
deadlocks and choose Redis locks if you need cross-process non-blocking
behavior.

⚠️ Potential issue | 🟠 Major

트랜잭션 롤백 처리 누락

OpenAI API 호출(line 165) 실패 시 이미 시작된 트랜잭션이 롤백되지 않고 열린 상태로 남을 수 있습니다. session.commit() (line 176) 전에 예외가 발생하면 세션이 일관되지 않은 상태가 됩니다.

try-except-finally 블록으로 트랜잭션 관리를 명확히 하세요:

 def get_weekly_result(session: Session, username: str, is_care: bool = False) -> str:
     """주간 종합분석 결과 메시지 생성"""
     from ..auth_service import get_auth_service
     auth = get_auth_service(session)
     owner = auth.get_user_by_username(username)
     if not owner:
         raise ValueError("user not found")
 
     # care인 경우 연결된 유저로 전환
     target_user = owner
     if is_care:
         if owner.role != 'CARE' or not owner.connecting_user_code:
             raise ValueError("invalid care user or not connected")
         target_user = auth.get_user_by_username(owner.connecting_user_code)
         if not target_user:
             raise ValueError("connected user not found")
 
     # 최신 voice_composite_id 조회
     latest_vc = (
         session.query(VoiceComposite.voice_composite_id)
         .join(Voice, Voice.voice_id == VoiceComposite.voice_id)
         .filter(Voice.user_id == target_user.user_id)
         .order_by(VoiceComposite.created_at.desc())
         .first()
     )
     latest_vc_id = latest_vc[0] if latest_vc else None
 
     # 캐시 조회
     cache = session.query(WeeklyResult).filter(WeeklyResult.user_id == target_user.user_id).first()
     if cache and cache.latest_voice_composite_id == latest_vc_id:
         return cache.message
 
+    try:
         # 생성 후 캐시 저장/갱신
         by_day = _query_weekly_top_emotions(session, target_user.user_id)
         messages = _build_weekly_prompt(target_user.name, by_day)
         msg = _call_openai(messages)
 
         if cache:
             cache.message = msg
             cache.latest_voice_composite_id = latest_vc_id
         else:
             session.add(WeeklyResult(
                 user_id=target_user.user_id,
                 latest_voice_composite_id=latest_vc_id,
                 message=msg,
             ))
         session.commit()
         return msg
+    except Exception:
+        session.rollback()
+        raise
🧰 Tools
🪛 Ruff (0.14.3)

136-136: Avoid specifying long messages outside the exception class

(TRY003)


142-142: Avoid specifying long messages outside the exception class

(TRY003)


145-145: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In app/services/analysis_service.py around lines 130-177, the OpenAI call and
subsequent DB write run inside an open transaction without rollback on failure;
wrap the generation and DB mutation logic (from _call_openai through
session.commit()) in a try/except that calls session.rollback() if any exception
occurs, optionally log or attach error context, then re-raise the exception so
callers handle it, and ensure session.commit() remains in the successful path
(or use try/except/finally to guarantee rollback on error and commit only on
success).

Comment on lines +180 to +226
def get_frequency_result(session: Session, username: str, is_care: bool = False) -> str:
"""월간 빈도 종합분석 결과 메시지 생성"""
from ..auth_service import get_auth_service
auth = get_auth_service(session)
owner = auth.get_user_by_username(username)
if not owner:
raise ValueError("user not found")

target_user = owner
if is_care:
if owner.role != 'CARE' or not owner.connecting_user_code:
raise ValueError("invalid care user or not connected")
target_user = auth.get_user_by_username(owner.connecting_user_code)
if not target_user:
raise ValueError("connected user not found")

# 최신 voice_composite_id 조회
latest_vc = (
session.query(VoiceComposite.voice_composite_id)
.join(Voice, Voice.voice_id == VoiceComposite.voice_id)
.filter(Voice.user_id == target_user.user_id)
.order_by(VoiceComposite.created_at.desc())
.first()
)
latest_vc_id = latest_vc[0] if latest_vc else None

# 캐시 조회
cache = session.query(FrequencyResult).filter(FrequencyResult.user_id == target_user.user_id).first()
if cache and cache.latest_voice_composite_id == latest_vc_id:
return cache.message

# 생성 후 캐시 저장/갱신
counts = _query_month_emotion_counts(session, target_user.user_id)
messages = _build_frequency_prompt(target_user.name, counts)
msg = _call_openai(messages)

if cache:
cache.message = msg
cache.latest_voice_composite_id = latest_vc_id
else:
session.add(FrequencyResult(
user_id=target_user.user_id,
latest_voice_composite_id=latest_vc_id,
message=msg,
))
session.commit()
return msg
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

get_frequency_result 함수에 동일한 이슈 존재

get_weekly_result에서 지적한 경쟁 조건 및 트랜잭션 롤백 문제가 이 함수에도 동일하게 존재합니다.

get_weekly_result에 적용한 수정사항을 이 함수에도 동일하게 적용하세요.

🧰 Tools
🪛 Ruff (0.14.3)

186-186: Avoid specifying long messages outside the exception class

(TRY003)


191-191: Avoid specifying long messages outside the exception class

(TRY003)


194-194: Avoid specifying long messages outside the exception class

(TRY003)

Comment on lines +25 to +40
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.UniqueConstraint('user_id', name='uq_weekly_user'),
)
op.create_index('idx_weekly_user', 'weekly_result', ['user_id'])
op.create_index('idx_weekly_latest_vc', 'weekly_result', ['latest_voice_composite_id'])

op.create_table(
'frequency_result',
sa.Column('frequency_result_id', sa.BigInteger(), primary_key=True, autoincrement=True),
sa.Column('user_id', sa.BigInteger(), sa.ForeignKey('user.user_id', ondelete='CASCADE'), nullable=False),
sa.Column('latest_voice_composite_id', sa.BigInteger(), sa.ForeignKey('voice_composite.voice_composite_id', ondelete='CASCADE'), nullable=True),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.UniqueConstraint('user_id', name='uq_freq_user'),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

updated_at 자동 갱신이 빠져 있습니다
이 테이블은 캐시된 분석 결과를 계속 갱신할 예정인데, 현재 migration에서는 updated_at 컬럼에 server_onupdate가 설정되지 않아 UPDATE 시각이 자동으로 변하지 않습니다. 이후 캐시 무효화나 정합성 판단에서 시간이 갱신되지 않아 오래된 데이터로 오인될 가능성이 큽니다. database_schema.sql에도 ON UPDATE CURRENT_TIMESTAMP가 명시된 만큼 migration에서도 동일하게 맞춰주세요. 아래처럼 server_onupdate=sa.text('CURRENT_TIMESTAMP')를 추가하면 해결됩니다.

-        sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
+        sa.Column(
+            'updated_at',
+            sa.DateTime(),
+            server_default=sa.text('CURRENT_TIMESTAMP'),
+            server_onupdate=sa.text('CURRENT_TIMESTAMP'),
+            nullable=False,
+        ),

두 번째 테이블(frequency_result)도 동일하게 수정해 주세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In migrations/versions/202511010003_add_weekly_frequency_result.py around lines
25 to 40, the updated_at columns for the weekly_result and frequency_result
tables lack automatic update behavior; add
server_onupdate=sa.text('CURRENT_TIMESTAMP') to each updated_at Column (i.e.,
change server_default=sa.text('CURRENT_TIMESTAMP') to also include
server_onupdate=sa.text('CURRENT_TIMESTAMP')) so UPDATE operations refresh the
timestamp and match database_schema.sql's ON UPDATE CURRENT_TIMESTAMP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant