Skip to content

Merge to main#14

Merged
jpark0506 merged 22 commits intomainfrom
develop
Nov 2, 2025
Merged

Merge to main#14
jpark0506 merged 22 commits intomainfrom
develop

Conversation

@H4nnhoi
Copy link
Contributor

@H4nnhoi H4nnhoi commented Nov 1, 2025

🔎 Description

refactoring & add functions(FCM, optimization speed when upload s3 by thread) & etc.

🔗 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

  • global exception
  • fcm function setting
  • optimization speed when upload voice file (by using thread)
  • add API(my profile, analyze result page)
  • edit connecting user when care sign up

🧪 Testing

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

Summary by CodeRabbit

  • 새로운 기능

    • 사용자·간병인 정보 조회, 로그아웃, 음성 업로드·목록·상세·삭제, FCM 토큰 등록/비활성화 및 관련 알림, 복합 감정 조회/재계산 API 추가
    • S3 미리서명 URL 제공, WAV 변환 기반 업로드 및 백그라운드 처리 파이프라인, 성능 로거 및 메모리 모니터 도입
    • 멀티모달 VA 융합 기반 감정 합성 및 자동 집계 알림 지원
  • 개선 사항

    • 감정 집계에서 미분류 제외 및 상위 감정 선정 로직 개선
    • 전역 예외 처리 강화 및 에러 응답 표준화
  • 기타

    • FCM 토큰 DB 스키마·마이그레이션 추가, 성능 로그 경로 .gitignore 반영, 의존성(메모리·FCM 관련) 추가

@H4nnhoi H4nnhoi self-assigned this Nov 1, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 1, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

음성 멀티모달 VA 퓨전 파이프라인, VoiceComposite/VoiceJobProcess/FcmToken 모델·마이그레이션, WAV 변환·S3 presigned URL, FCM 멀티캐스트, 성능 로거·메모리 모니터, 예외 계층 및 전역 핸들러, 관련 리포지토리·서비스·API 엔드포인트가 추가되었습니다.

Changes

코호트 / 파일(s) 요약
모델·마이그레이션·스키마
app/models.py, migrations/versions/..._add_voice_composite.py, migrations/versions/..._add_fcm_token.py, database_schema.sql
VoiceComposite, VoiceJobProcess, FcmToken 모델 추가; Voice에 1:1 관계 추가; 마이그레이션과 전체 SQL 스키마 추가
서비스 — VA 퓨전·합성
app/services/va_fusion.py, app/services/composite_service.py
audio 확률 + text 감정을 결합하는 fuse_VA 및 CompositeService(계산·업서트) 추가
서비스 — 음성 처리·업로드
app/voice_service.py, app/s3_service.py
WAV 변환 및 S3 업로드 워크플로우, 배경 STT/NLP·감정 분석 작업, presigned S3 URL 생성·포함, upload_fileobj에 content_type 옵션
서비스 — FCM / 알림
app/services/fcm_service.py
Firebase Admin 싱글턴 초기화, 멀티캐스트 전송 및 실패 토큰 비활성화 로직 추가
리포지토리·DB 유틸
app/repositories/composite_repo.py, app/repositories/job_repo.py, app/repositories/voice_repo.py, app/repositories/fcm_repo.py
VoiceComposite upsert, VoiceJobProcess 관리(ensure/mark/try_aggregate), 오디오·텍스트 조회 유틸, FCM 토큰 등록·비활성화·조회 구현
API·라우터·메인 변경
app/routers/composite_router.py, app/main.py
voice composite 라우터 추가, 다수 엔드포인트(사용자/간병인 조회, FCM 등록/비활성화, 감정 빈도/주간 요약 등) 및 composite_router 포함
DTO·예외·글로벌 핸들러
app/dto.py, app/exceptions.py
UserInfo/CareInfo/FcmToken DTO 추가, AppException 계층 및 구체적 예외 클래스 추가, 전역 예외 핸들러 확장
유틸 — 성능·메모리 로깅
app/performance_logger.py, app/memory_monitor.py, .gitignore
per-voice PerformanceLogger, performance_logs 디렉터리 .gitignore 추가, 프로세스·시스템 메모리 조회·로깅·임계치 검사 유틸 추가
요구사항·환경
requirements.txt
bcrypt, alembic, psutil, firebase-admin 등 의존성 추가
문서·규칙 파일
.cursorrules
DB 세션/DI 관련 코딩 규칙 문서 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant API as API Endpoint
    participant VoiceService
    participant S3
    participant DB
    participant JobRepo as Job Repo
    participant STT as STT/NLP Thread
    participant Emotion as Emotion Thread
    participant Composite as CompositeService
    participant FCM as FCM Service

    User->>API: POST /users/voices (파일 업로드)
    API->>VoiceService: upload_user_voice()
    VoiceService->>VoiceService: WAV 변환, 성능 로그 시작
    VoiceService->>S3: WAV 업로드
    VoiceService->>DB: Voice 레코드 생성
    VoiceService->>JobRepo: ensure_job_row()
    VoiceService->>API: voice_id 반환
    API-->>User: 201 Created

    par 비동기 처리
        VoiceService->>STT: _process_stt_and_nlp_background()
        STT->>DB: VoiceContent 저장
        STT->>JobRepo: mark_text_done()
        
        VoiceService->>Emotion: _process_audio_emotion_background()
        Emotion->>DB: VoiceAnalyze 저장
        Emotion->>JobRepo: mark_audio_done()
    end

    JobRepo->>JobRepo: try_aggregate() (FOR UPDATE 락)
    JobRepo->>Composite: compute_and_save_composite(voice_id)
    Composite->>DB: audio/text 데이터 조회
    Composite->>Composite: fuse_VA()
    Composite->>DB: VoiceComposite 업서트
    JobRepo->>FCM: _send_composite_completion_notification()
    FCM->>DB: 토큰 조회 및 multicast 전송
Loading
sequenceDiagram
    participant Care
    participant API
    participant CareService
    participant DB

    Care->>API: GET /care/users/voices/analyzing/weekly?month=&week=
    API->>CareService: get_emotion_weekly_summary()
    CareService->>DB: 연결 사용자 음성 및 VoiceComposite 조회
    loop 각 날짜
        CareService->>CareService: 비-Unknown 필터 적용
        alt 비-Unknown 존재
            CareService->>CareService: 비-Unknown 중 최상위 선택 (tie-break day-first)
        else 모두 Unknown
            CareService->>CareService: 빈도 기반 최다 감정 선택
        end
    end
    CareService->>API: 날짜별 top_emotion 응답
    API-->>Care: 200 JSON
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • 주의 깊이 검토할 파일/영역:
    • app/services/va_fusion.py (가중치·정규화·마스킹 수학 로직)
    • app/repositories/job_repo.py (행 잠금, 트랜잭션, 예외 복구)
    • app/voice_service.py (WAV 변환, 스레드/비동기 조합, 퍼포먼스 로거 상태 관리)
    • app/services/fcm_service.py (Firebase 초기화, 멀티캐스트 오류 처리 및 토큰 비활성화)
    • 마이그레이션·database_schema.sql의 제약조건과 기존 데이터 호환성

Possibly related PRs

  • Merge to main #14 — 동일한 기능·파일 집합(VoiceComposite/FCM/VA 퓨전/성능 로거 등)을 포함한 PR로 직접 관련.
  • Merge to main #11 — care_service의 월간·주간 감정 집계 로직 변경과 중복되는 코드 레이어가 있어 관련.
  • hotfix #13 — voice_service·care_service·모델 변경과 surprise_bps 처리 등 구현상 중복 및 연관 지점 존재.

Poem

🐰 작은 토끼가 전하네:
파형을 읽고 마음을 섞어,
성능은 기록하고 알림은 울려요,
WAV도 변환하고 토큰도 챙기며,
한걸음씩 서비스가 자라나요 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.74% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive PR 제목인 "Merge to main"은 매우 모호하고 일반적인 용어로, 변경사항에 대해 의미 있는 정보를 전달하지 못합니다. 실제 변경사항을 보면 전역 예외 처리 추가, FCM 기능 설정, S3 업로드 최적화, 새로운 API 추가, 사용자 연결 로직 편집 등이 있으나, 제목에서는 이러한 주요 내용을 전혀 반영하지 않고 있습니다. 단순히 병합 작업을 나타내는 용어로만 남아있어서 코드 리뷰 히스토리를 훑을 때 변경사항의 본질을 파악하기 어렵습니다. 제목을 변경사항의 주요 내용을 명확히 반영하도록 수정하는 것을 권장합니다. 예를 들어 "Add FCM support and optimize voice upload with threading" 또는 "Refactor authentication and add voice analysis APIs" 같이 구체적이고 간결한 표현으로 개선하면 좋겠습니다.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed PR 설명이 템플릿 구조를 대부분 따르고 있으며, 필수 섹션인 Description, What type of PR, Changes Made, Testing이 모두 채워져 있습니다. Description에서는 주요 변경사항을 간단히 언급하고 있고, Changes Made 섹션에서는 5개의 구체적인 변경 항목을 체크박스로 명시하고 있습니다. 다만 Description이 다소 간결하고 Testing 섹션에서 Integration tests와 Manual testing이 체크되지 않은 상태입니다. 그러나 전반적으로 필요한 주요 정보가 포함되어 있어 검토자가 변경사항을 이해하기에 충분합니다.

📜 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 7a2229a and a18c2df.

📒 Files selected for processing (1)
  • .cursorrules (1 hunks)

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

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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/auth_service.py (1)

45-118: 치명적: CARE 연결 username을 20자 컬럼에 저장하면 즉시 실패

이제 connecting_user_code에 USER의 username을 저장하도록 바뀌었지만, app/models.py에서 해당 컬럼은 여전히 String(20)(VARCHAR(20))으로 정의돼 있습니다. USER username은 64자까지 허용되므로 21자 이상 username을 연결하는 순간 MySQL에서는 DataError가 나거나(STRICT 모드) 무음으로 잘려 실제 username과 불일치합니다. CARE 회원가입·조회가 바로 깨지므로, 컬럼 길이를 username과 동일한 64자로 확장하고 마이그레이션을 동반하거나 기존대로 user_code를 사용하도록 로직을 되돌려야 합니다.

🧹 Nitpick comments (5)
requirements.txt (3)

15-18: 버전 범위 지정 방식 검토 권장.

모든 의존성이 >= 연산자를 사용하여 하한만 지정되어 있습니다. 프로덕션 환경에서는 주요 의존성의 상한을 지정하거나 특정 마이너 버전으로 핀딩하는 것이 권장됩니다. 특히 firebase-admin, torch, transformers 등 주요 라이브러리는 더 엄격한 버전 제약이 필요할 수 있습니다.

예를 들어:

-firebase-admin>=6.0.0
+firebase-admin>=6.0.0,<7.0.0

[scratchpad_end] -->


1-18: 의존성 설치 테스트 권장.

새로운 의존성 4개가 추가되었으므로, 모든 의존성이 호환되고 충돌이 없는지 확인하기 위해 설치 테스트를 수행할 것을 권장합니다. 특히 torchtransformers 같은 무거운 의존성과 새로 추가된 의존성 간의 호환성을 확인하세요.

다음 명령으로 의존성 호환성을 테스트할 수 있습니다:

pip install -r requirements.txt --dry-run

[scratchpad_end] -->

Comment on lines +23 to 25
user = self.db.query(User).filter(User.username == care.connecting_user_code).first()
if not user:
return {"success": False, "frequency": {}, "message": "Connected user not found."}
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.connecting_user_code 컬럼에는 말 그대로 user_code 값이 저장되어 왔는데, 여기서 User.username으로 비교하도록 바꾸면 기존 데이터는 전부 조회에 실패하여 보호자용 월간/주간 감정 통계가 전부 빈 결과를 돌려주게 됩니다. 새로운 가입 흐름에서 username을 저장하도록 바꾸었더라도, 과거 레코드를 일괄 마이그레이션하지 않으면 바로 회귀가 발생합니다. 안전하게 User.user_code 비교를 유지하거나, 저장되는 컬럼 자체를 username으로 교체하고 기존 데이터를 모두 갱신하는 과정을 함께 포함시켜 주세요.

Also applies to: 58-60

🤖 Prompt for AI Agents
In app/care_service.py around lines 23-25 (and similarly at lines 58-60), the
code compares care.connecting_user_code to User.username which breaks lookup for
existing records because the legacy column stores user_code; change the query to
compare against User.user_code (or alternatively, only do this if you've
included a full data migration and column rename) so existing data continues to
match—update both places to use User.user_code or implement and document a
migration that renames/moves values to username before switching the comparison.

Comment on lines +100 to 110
# 2) 연결된 사용자 조회 (username으로 조회)
linked_user = self.get_user_by_username(care.connecting_user_code)
if not linked_user:
return []
# 3) 연결 사용자 음성 중 분석 완료만(join) 페이징
q = (
self.db.query(Voice)
.join(VoiceAnalyze, VoiceAnalyze.voice_id == Voice.voice_id)
.filter(Voice.user_id == linked_user.user_id)
.options(joinedload(Voice.questions), joinedload(Voice.voice_analyze))
.options(joinedload(Voice.questions), joinedload(Voice.voice_analyze), joinedload(Voice.voice_composite))
.order_by(Voice.created_at.desc())
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 연결 사용자 조회가 끊어집니다.

여기서도 care.connecting_user_code를 username으로 바라보고 있는데, 컬럼 명세는 여전히 user_code를 담는 형태라서 기존 연결 정보가 그대로라면 보호자가 연결된 사용자의 음성을 한 건도 조회하지 못합니다. 위 흐름과 동일하게 조회 키를 User.user_code로 유지하거나, 저장 데이터/마이그레이션까지 함께 반영해 주세요.

🤖 Prompt for AI Agents
In app/db_service.py around lines 100 to 110, the code is using
care.connecting_user_code as if it were a username which breaks lookup because
the stored value is actually user_code; change the lookup to use the user_code
column (e.g., call or implement get_user_by_user_code or a similar method that
queries User.user_code == care.connecting_user_code) so the linked_user resolves
correctly and existing connections return voice records, or if you intend to
migrate stored values then update the stored data/migration accordingly.

Comment on lines 125 to 138
# 5. 데이터베이스 저장 (기본 정보만)
# 파일 크기로 대략적인 duration 추정
file_size_mb = len(file_content) / (1024 * 1024)
file_size_mb = len(wav_content) / (1024 * 1024)
estimated_duration_ms = int(file_size_mb * 1000) # 대략적인 추정

# Voice 저장 (STT 없이 기본 정보만)
voice = self.db_service.create_voice(
voice_key=key,
voice_name=file.filename,
voice_name=wav_filename,
duration_ms=estimated_duration_ms,
user_id=user.user_id,
sample_rate=16000 # 기본값
)

# 5. 비동기 후처리 (STT→NLP, 음성 감정 분석)
asyncio.create_task(self._process_stt_and_nlp_background(file_content, file.filename, voice.voice_id))
asyncio.create_task(self._process_audio_emotion_background(file_content, file.filename, voice.voice_id))
# ensure job row
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

WAV 길이 추정 로직이 잘못되었습니다.

WAV 파일 크기를 MB→ms로 그대로 곱해서 길이를 추정하면 16kHz PCM 기준으로 실제 길이 대비 약 1/32 수준만 측정됩니다. 예를 들어 10초짜리 음성(약 0.3MB)은 300ms로 기록되어 리스트/상세 UI, 통계, 집계 로직에 모두 잘못된 시간이 반영됩니다. WAV 바이트를 다시 열어 프레임 수와 샘플레이트로 정확한 길이를 계산해 주세요.

-            file_size_mb = len(wav_content) / (1024 * 1024)
-            estimated_duration_ms = int(file_size_mb * 1000)  # 대략적인 추정
+            with sf.SoundFile(BytesIO(wav_content)) as wav_file:
+                frames = len(wav_file)
+                sample_rate = wav_file.samplerate
+            estimated_duration_ms = int(frames / sample_rate * 1000)

위 계산은 upload_user_voiceupload_voice_with_question 두 곳 모두에 동일하게 적용해야 합니다.

Also applies to: 565-574

🤖 Prompt for AI Agents
In app/voice_service.py around lines 125-138 (and also apply same fix at
565-574), the current WAV duration estimate uses file size which is incorrect;
instead open the WAV bytes with wave (or librosa/soundfile) via io.BytesIO, read
n_frames and framerate and compute duration_ms = int((n_frames / framerate) *
1000), and use that duration_ms when creating the voice record (also set
sample_rate to the WAV framerate); ensure you handle invalid WAV data with a
clear error/log and fallback behavior as needed.

Comment on lines +143 to +152
original_start = logger.start_time
clear_logger(0)
logger = get_performance_logger(voice.voice_id, preserve_time=original_start)
# 기존 단계들 복사
for step in ["시작", "파일변환 완료", "s3업로드 완료", "데이터베이스 입력 완료"]:
if step in logger.steps:
continue
logger.steps[step] = time.time() - original_start
logger.voice_id = voice.voice_id

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

초기 단계 로그가 요약에서 사라집니다.

clear_logger(0) 이후 새 로거로 갈아끼우면서 logger.steps만 수동으로 채우고 step_orderstep_category를 복원하지 않아, get_summary() 결과에서 “시작/파일변환/s3업로드/데이터베이스 입력” 단계가 전부 누락됩니다. 아래처럼 기존 순서와 카테고리를 저장해 add_step_with_time으로 복원하면 일관성을 유지할 수 있습니다.

-            original_start = logger.start_time
-            clear_logger(0)
-            logger = get_performance_logger(voice.voice_id, preserve_time=original_start)
-            for step in ["시작", "파일변환 완료", "s3업로드 완료", "데이터베이스 입력 완료"]:
-                if step in logger.steps:
-                    continue
-                logger.steps[step] = time.time() - original_start
+            original_start = logger.start_time
+            existing_steps = dict(logger.steps)
+            existing_order = list(logger.step_order)
+            existing_categories = dict(logger.step_category)
+            clear_logger(0)
+            logger = get_performance_logger(voice.voice_id, preserve_time=original_start)
+            for step in existing_order:
+                elapsed = existing_steps[step]
+                category = existing_categories.get(step, "serial")
+                logger.add_step_with_time(step, elapsed, category)
             logger.voice_id = voice.voice_id

이렇게 하면 파일 업로드 단계가 performance 로그 요약에 온전히 남습니다.

🤖 Prompt for AI Agents
In app/voice_service.py around lines 143 to 152, after calling clear_logger(0)
you only repopulate logger.steps and miss restoring logger.step_order and
logger.step_category which causes those initial steps to be omitted from
get_summary(); capture the original step_order and step_category before
clearing, then after creating the new logger iterate the original ordered steps
and call add_step_with_time(step_name, timestamp - original_start,
category=original_category) (or equivalent helper) to restore both the ordering
and categories along with times, and finally set logger.voice_id as before.

Comment on lines +5 to +6
revision = '202510310001_add_voice_composite'
down_revision = None # set to current head in real chain if needed
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

down_revision을 기존 헤드로 연결해야 합니다.
현재 down_revision = None으로 남아 있어서 이 마이그레이션이 독립된 루트 헤드로 인식됩니다. 이미 voice 테이블이 존재하는 다른 마이그레이션 체인이 있다는 전제에서, 이 상태로는 Alembic이 두 개의 시작점을 갖게 되어 alembic upgrade head가 실패하거나 예기치 않은 분리 브랜치가 생깁니다. 기존 최신 헤드 revision ID로 down_revision을 정확히 설정해 주세요.

- down_revision = None  # set to current head in real chain if needed
+ down_revision = "<현재 헤드 revision ID로 교체>"

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

🤖 Prompt for AI Agents
In migrations/versions/202510310001_add_voice_composite.py around lines 5 to 6,
the down_revision is set to None which makes this migration an independent root;
update down_revision to the current latest head revision ID from your Alembic
revision history (e.g., check alembic/versions or run alembic history/current to
get the exact revision string) and replace None with that revision ID so this
file links to the existing migration chain.

Comment on lines +15 to +18
bcrypt>=4.0.0
alembic>=1.12.0
psutil>=5.9.0
firebase-admin>=6.0.0
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

새로운 의존성의 버전과 호환성 검증 필요.

추가된 의존성들이 PR 목표(FCM 설정, 데이터베이스 마이그레이션, 성능 모니터링, 인증 강화)와 일치하나, 버전이 최신이고 보안 문제가 없는지 확인이 필요합니다. 특히 firebase-admin>=6.0.0의 경우 최신 버전을 사용하는 것이 좋습니다.

다음을 확인해주시기 바랍니다:

[scratchpad_end] -->


🌐 Web query:

firebase-admin 최신 버전 및 보안 상태

💡 Result:

요약: 2025-08-28 기준 Firebase Admin Node.js SDK 최신 안정버전은 v13.5.0이며(릴리스 노트·GitHub), 현재 공개된 직접적인 취약점은 보고되지 않았습니다(취약점 DB·Snyk). 과거 다른 언어용 Admin SDK(예: PHP)에서 발견된 CVE는 있으나 Node.js 최신 릴리스에는 직접 영향이 없는 것으로 보입니다. 권장사항: 최신 버전(v13.5.0)으로 유지하고 의존성(간접 의존성)·보안 공지(OSS 어드바이저·GitHub Advisory)를 주기적으로 모니터링하세요.

근거:

  • Firebase 릴리스 노트(관리자 SDK 업데이트 기록, 2025-05·2025-10 등). [1]
  • firebase-admin-node GitHub 릴리스 (v13.5.0 — 2025-08-28). [2]
  • Snyk 패키지/취약점 확인: firebase-admin 최신 버전(13.5.0)에 직접 보고된 취약점 없음(간접 의존성은 별도 확인 권장). [3]
  • 과거 취약점 예시: GHSA/CVE (Firebase Admin SDK for PHP 관련 CVE-2018-1000025). [4]

참고(원하시면 바로 확인해 드립니다):

  • 현재 프로젝트의 package.json/package-lock 또는 npm audit·Snyk/OSS Index 스캔 결과를 분석해 간접 의존성 취약 여부를 점검해 드릴 수 있습니다.

🌐 Web query:

bcrypt 4.0.0 이상 버전의 호환성

💡 Result:

짧게 정리합니다 — 어떤 언어/패키지인지에 따라 의미가 다릅니다.

  • Node.js용 (npm) bcrypt (node.bcrypt.js)

    • 패키지별 호환성 표: 오래된 4.x 버전은 Node 최신 버전과 호환이 떨어집니다. 예: Node 18 이상에서는 bcrypt >= 6.0.0이 권장/필요합니다. 자세한 호환표는 공식 repo 참조. [1]
  • Python용 bcrypt (PyPI)

    • bcrypt 4.0.0부터 구현이 Rust로 변경되어(소스에서 빌드하려면 Rust 컴파일러 필요) 윈도우/리눅스 등에서 휠(wheel)이 없으면 빌드 도구(Rust)가 필요합니다. 또한 manylinux wheel 정책이 변경되었습니다. [2]
  • 보안/취약점 주의

    • bcrypt < 5.0.0 계열은 NUL 처리·데이터 잘림 등 취약점이 보고되어 있으므로 가능한 경우 5.0.0 이상(또는 최신 안정 버전)으로 업그레이드 권장. [3]

원하시면 (a) 사용 중인 언어/npm 패키지 이름(bcrypt 또는 bcryptjs 등), (b) 현재 버전과 Node/Python 버전을 알려주시면 구체적으로 업그레이드/대체(예: bcryptjs, bcrypt-ts, prebuilt wheel 사용) 방안과 명령을 바로 제시하겠습니다.

참고자료

  1. node.bcrypt.js GitHub - 호환표 및 설명. [1]
  2. PyPI bcrypt 4.2.0 릴리즈 노트(4.0.0 변경점: Rust 구현 등). [2]
  3. Snyk 취약점 정보(bcrypt 관련 권고). [3]


bcrypt 버전을 5.0.0 이상으로 업그레이드 필요. firebase-admin 및 기타 의존성도 최신 버전 검토 권장.

bcrypt>=4.0.0은 보안 취약점(NUL 처리, 데이터 잘림 등)이 보고되어 있으므로 5.0.0 이상 업그레이드 권장합니다.

추가로 firebase-admin의 최신 안정 버전은 v13.5.0(2025-08-28 기준)이며 현재 공개된 직접적인 취약점은 보고되지 않았습니다. 현재 지정된 firebase-admin>=6.0.0은 매우 낮은 버전이므로 최신 버전 사용을 검토하세요. 의존성 및 보안 공지를 주기적으로 모니터링하길 권장합니다.

🤖 Prompt for AI Agents
In requirements.txt around lines 15–18, upgrade bcrypt to at least 5.0.0 and
review/pin newer stable versions for firebase-admin and other low-version deps:
replace the bcrypt line with a constraint of bcrypt>=5.0.0 (or a specific tested
version like bcrypt==5.x.y), update firebase-admin to a recent stable release
(e.g., >=13.5.0) or a specific tested version, and run your dependency
installation, test suite, and a security audit (pip-audit or OWASP
dependency-check) to ensure compatibility; if your project uses a lockfile or
dependency tool (pip-compile/poetry), update and regenerate that lockfile and
resolve any breaking changes in code that depend on bcrypt or firebase-admin
APIs.

@jpark0506 jpark0506 merged commit 154e41e into main Nov 2, 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: 2

♻️ Duplicate comments (1)
app/voice_service.py (1)

567-574: 파일 크기로 길이를 추정하면 duration이 크게 틀어집니다.
동일한 WAV라도 비트레이트에 따라 길이 추정이 크게 빗나가, 리스트·통계·집계에 잘못된 시간이 저장됩니다. 바로 위 upload_user_voice처럼 프레임 수와 샘플레이트로 정확히 계산해 주세요.

-            file_size_mb = len(wav_content) / (1024 * 1024)
-            estimated_duration_ms = int(file_size_mb * 1000)
+            with sf.SoundFile(BytesIO(wav_content)) as wav_file:
+                frames = len(wav_file)
+                sr = wav_file.samplerate
+            estimated_duration_ms = int((frames / sr) * 1000)
🧹 Nitpick comments (1)
app/voice_service.py (1)

145-154: 성능 로그 복원 시 단계 순서와 카테고리가 사라집니다.
logger.steps만 직접 채우면 step_orderstep_category가 비어 있어 요약에서 초기 단계가 누락됩니다. 아래와 같이 기존 순서·카테고리까지 백업 후 add_step_with_time으로 복원해 주세요.

-            original_start = logger.start_time
-            clear_logger(0)
-            logger = get_performance_logger(voice.voice_id, preserve_time=original_start)
-            for step in ["시작", "파일변환 완료", "s3업로드 완료", "데이터베이스 입력 완료"]:
-                if step in logger.steps:
-                    continue
-                logger.steps[step] = time.time() - original_start
-            logger.voice_id = voice.voice_id
+            original_start = logger.start_time
+            existing_steps = dict(logger.steps)
+            existing_order = list(logger.step_order)
+            existing_categories = dict(logger.step_category)
+            clear_logger(0)
+            logger = get_performance_logger(voice.voice_id, preserve_time=original_start)
+            for step in existing_order:
+                elapsed = existing_steps[step]
+                category = existing_categories.get(step, "serial")
+                logger.add_step_with_time(step, elapsed, category)
+            logger.voice_id = voice.voice_id
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc9fa40 and 7a2229a.

📒 Files selected for processing (6)
  • app/main.py (12 hunks)
  • app/repositories/fcm_repo.py (1 hunks)
  • app/repositories/job_repo.py (1 hunks)
  • app/routers/composite_router.py (1 hunks)
  • app/services/fcm_service.py (1 hunks)
  • app/voice_service.py (17 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/repositories/fcm_repo.py
🧰 Additional context used
🧬 Code graph analysis (5)
app/repositories/job_repo.py (4)
app/models.py (3)
  • VoiceJobProcess (183-194)
  • Voice (32-58)
  • User (8-29)
app/services/composite_service.py (2)
  • CompositeService (8-34)
  • compute_and_save_composite (12-34)
app/services/fcm_service.py (2)
  • FcmService (40-174)
  • send_notification_to_user (47-75)
app/performance_logger.py (4)
  • get_performance_logger (109-115)
  • log_step (22-37)
  • save_to_file (91-102)
  • clear_logger (118-121)
app/voice_service.py (10)
app/s3_service.py (2)
  • upload_fileobj (26-32)
  • get_presigned_url (45-57)
app/stt_service.py (1)
  • transcribe_voice (144-146)
app/nlp_service.py (1)
  • analyze_text_sentiment (202-204)
app/emotion_service.py (1)
  • analyze_voice_emotion (225-227)
app/db_service.py (3)
  • get_db_service (307-309)
  • create_voice (45-60)
  • get_user_by_username (32-34)
app/auth_service.py (2)
  • get_auth_service (297-299)
  • get_user_by_username (144-146)
app/repositories/job_repo.py (4)
  • ensure_job_row (64-71)
  • mark_text_done (74-77)
  • mark_audio_done (80-83)
  • try_aggregate (86-136)
app/performance_logger.py (5)
  • get_performance_logger (109-115)
  • clear_logger (118-121)
  • log_step (22-37)
  • save_to_file (91-102)
  • add_step_with_time (39-44)
app/models.py (2)
  • VoiceAnalyze (85-110)
  • Voice (32-58)
app/memory_monitor.py (1)
  • log_memory_info (59-77)
app/services/fcm_service.py (2)
app/repositories/fcm_repo.py (2)
  • get_active_fcm_tokens_by_user (71-76)
  • get_active_fcm_tokens_by_username (79-84)
app/models.py (1)
  • FcmToken (197-219)
app/routers/composite_router.py (3)
app/database.py (1)
  • get_db (38-44)
app/services/composite_service.py (2)
  • CompositeService (8-34)
  • compute_and_save_composite (12-34)
app/models.py (1)
  • VoiceComposite (146-180)
app/main.py (10)
app/s3_service.py (2)
  • list_bucket_objects (35-42)
  • list_bucket_objects_with_urls (60-82)
app/database.py (1)
  • get_db (38-44)
app/models.py (3)
  • Question (113-128)
  • VoiceComposite (146-180)
  • Voice (32-58)
app/dto.py (6)
  • UserInfoResponse (38-42)
  • CareInfoResponse (45-49)
  • FcmTokenRegisterRequest (53-57)
  • FcmTokenRegisterResponse (60-64)
  • FcmTokenDeactivateResponse (67-70)
  • UserVoiceDetailResponse (126-132)
app/care_service.py (3)
  • CareService (7-118)
  • get_emotion_monthly_frequency (12-44)
  • get_emotion_weekly_summary (46-118)
app/exceptions.py (6)
  • AppException (6-10)
  • ValidationException (13-16)
  • RuntimeException (19-22)
  • DatabaseException (25-28)
  • OutOfMemoryException (31-34)
  • InternalServerException (37-40)
app/memory_monitor.py (2)
  • get_memory_info (9-47)
  • log_memory_info (59-77)
app/auth_service.py (3)
  • get_user_by_username (144-146)
  • get_user_info (207-251)
  • get_care_info (253-294)
app/repositories/fcm_repo.py (3)
  • deactivate_fcm_tokens_by_user (46-53)
  • register_fcm_token (7-43)
  • deactivate_fcm_token_by_device (56-68)
app/voice_service.py (4)
  • get_user_voice_list (363-432)
  • get_user_voice_detail (451-487)
  • get_user_emotion_monthly_frequency (623-647)
  • get_user_emotion_weekly_summary (649-709)
🪛 Ruff (0.14.2)
app/repositories/job_repo.py

58-58: Do not catch blind exception: Exception

(BLE001)


61-61: Use explicit conversion flag

Replace with conversion flag

(RUF010)


123-123: Do not catch blind exception: Exception

(BLE001)


126-126: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


126-126: Use explicit conversion flag

Replace with conversion flag

(RUF010)


133-133: Consider moving this statement to an else block

(TRY300)

app/voice_service.py

46-46: Unpacked variable sr is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


63-63: Do not use bare except

(E722)


63-64: try-except-pass detected, consider logging the exception

(S110)


68-68: Do not use bare except

(E722)


68-69: try-except-pass detected, consider logging the exception

(S110)


160-160: Store a reference to the return value of asyncio.create_task

(RUF006)


161-161: Store a reference to the return value of asyncio.create_task

(RUF006)


163-167: Consider moving this statement to an else block

(TRY300)


168-168: Do not catch blind exception: Exception

(BLE001)


174-174: Use explicit conversion flag

Replace with conversion flag

(RUF010)


261-261: Undefined name k

(F821)


262-263: try-except-pass detected, consider logging the exception

(S110)


262-262: Do not catch blind exception: Exception

(BLE001)


601-601: Store a reference to the return value of asyncio.create_task

(RUF006)


602-602: Store a reference to the return value of asyncio.create_task

(RUF006)


620-620: Use explicit conversion flag

Replace with conversion flag

(RUF010)


631-631: Do not catch blind exception: Exception

(BLE001)


645-645: Consider moving this statement to an else block

(TRY300)


646-646: Do not catch blind exception: Exception

(BLE001)


647-647: Use explicit conversion flag

Replace with conversion flag

(RUF010)


657-657: Do not catch blind exception: Exception

(BLE001)


707-707: Consider moving this statement to an else block

(TRY300)


708-708: Do not catch blind exception: Exception

(BLE001)


709-709: Use explicit conversion flag

Replace with conversion flag

(RUF010)

app/services/fcm_service.py

30-33: Avoid specifying long messages outside the exception class

(TRY003)


157-157: Do not catch blind exception: Exception

(BLE001)

app/routers/composite_router.py

11-11: 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)


15-15: Do not catch blind exception: Exception

(BLE001)


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

(B904)


16-16: Use explicit conversion flag

Replace with conversion flag

(RUF010)


20-20: 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)

app/main.py

46-46: Unused function argument: request

(ARG001)


65-65: Unused function argument: request

(ARG001)


86-86: Unused function argument: request

(ARG001)


99-99: Unused function argument: request

(ARG001)


106-106: Use explicit conversion flag

Replace with conversion flag

(RUF010)


112-112: Unused function argument: request

(ARG001)


119-119: Use explicit conversion flag

Replace with conversion flag

(RUF010)


125-125: Unused function argument: request

(ARG001)


132-132: Use explicit conversion flag

Replace with conversion flag

(RUF010)


138-138: Unused function argument: request

(ARG001)


236-236: 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)


258-258: 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)


277-277: 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)


299-299: 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)


312-312: 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)


318-318: 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)


333-333: 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)


345-345: 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)


362-362: 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)


371-371: 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)


384-384: 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)


411-411: Do not catch blind exception: Exception

(BLE001)


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

(B904)


412-412: Use explicit conversion flag

Replace with conversion flag

(RUF010)


419-419: 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)


447-447: 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)


457-457: 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)


471-471: 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)


484-484: 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)


491-491: 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)


504-504: 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)


511-511: 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)


536-536: Value being cast to int is already an integer

Remove unnecessary int call

(RUF046)


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

(TRY003)

Comment on lines +51 to +52
if status_code in (400, 401, 403, 404, 422):
status_code = 400
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

HTTPException 상태코드를 400으로 덮어쓰면 REST 계약이 깨집니다.
401·403·404 같은 구분이 모두 400으로 변해 호출자가 인증/인가 실패와 존재하지 않는 자원을 식별할 수 없게 됩니다. 최소한 422만 400으로 매핑하도록 좁혀 주세요.

-    if status_code in (400, 401, 403, 404, 422):
-        status_code = 400
+    if status_code == 422:
+        status_code = 400
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if status_code in (400, 401, 403, 404, 422):
status_code = 400
if status_code == 422:
status_code = 400
🤖 Prompt for AI Agents
In app/main.py around lines 51-52, the code currently maps multiple
HTTPException status codes (400, 401, 403, 404, 422) to 400 which breaks the
REST contract; change the logic to only remap 422 to 400 and leave 401, 403, 404
(and other non-422 codes) untouched so callers can distinguish
auth/permission/not-found errors; update the conditional to check specifically
for 422 and set status_code = 400 only in that case.

Comment on lines +22 to +25
if not row:
raise HTTPException(status_code=404, detail="not found")
return {
"voice_id": voice_id,
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 연결 사용자 조회 조건이 잘못되어 알림이 항상 건너뜁니다.
User.connecting_user_code 칼럼에는 보호자가 입력한 사용자 코드가 저장되는데, 현재 username과 비교해서 매칭이 절대 성립하지 않습니다. 그 결과 FCM 알림이 전혀 발송되지 않습니다. user_code와 비교하도록 수정해 주세요.

-        User.connecting_user_code == user.username
+        User.connecting_user_code == user.user_code

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

🤖 Prompt for AI Agents
In app/routers/composite_router.py around lines 22 to 25, the lookup uses
User.connecting_user_code compared against the requesting user's username so the
condition never matches; change the query to compare User.connecting_user_code
to the provided user_code (or the variable holding the guardian-entered code)
instead of username, so the row is found and FCM notifications are sent; update
any variable name usage to reflect user_code and keep the 404 behavior if no row
is returned.

This was referenced Nov 3, 2025
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.

2 participants