Skip to content

[OTT-277][FEAT]: flyway 신규 버전 및 엔티티 추가#158

Merged
yubin012 merged 7 commits intodevelopfrom
OT-277-feature/tagging-ml
Mar 12, 2026
Merged

[OTT-277][FEAT]: flyway 신규 버전 및 엔티티 추가#158
yubin012 merged 7 commits intodevelopfrom
OT-277-feature/tagging-ml

Conversation

@marulog
Copy link
Copy Markdown
Collaborator

@marulog marulog commented Mar 12, 2026

📝 작업 내용

이번 PR에서 작업한 내용을 적어주세요

  • flyway version 8 및 엔티티 생성

📷 스크린샷

☑️ 체크 리스트

체크 리스트를 확인해주세요

  • 테스트는 잘 통과했나요?
  • 충돌을 해결했나요?
  • 이슈는 등록했나요?
  • 라벨은 등록했나요?

#️⃣ 연관된 이슈

close #151

💬 리뷰 요구사항

env 수정된거 있습니다. 노션 참고바랍니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • AI 기반 미디어 무드 태깅 시스템 추가로 자동 감정 분석 및 분류 지원
    • 구조화된 오류 응답 형식으로 API 안정성 강화
    • 향상된 API 메타데이터 및 문서화로 개발자 경험 개선
    • 무드 카테고리 및 태그 관리 기능 구현
  • 개선사항

    • FastAPI 애플리케이션 전역 예외 처리 시스템 구축
    • 심층 학습 기반 감정 분석 모델 통합으로 태깅 정확도 향상

@marulog marulog self-assigned this Mar 12, 2026
@marulog marulog added feat 새로운 기능 구현 fix 버그 수정 labels Mar 12, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 12, 2026

Walkthrough

이 PR은 머신러닝 모델 기반의 감정 태깅 시스템을 구현합니다. 비동기 처리, 오류 처리 프레임워크, 새로운 데이터베이스 스키마, API 엔드포인트 재설계를 포함하며 PyTorch 모델을 백엔드와 통합합니다.

Changes

Cohort / File(s) Summary
Python ML 감정 태거 구현
apps/machine/app/services/tagging.py
PyTorch/Transformers 기반 MoodTagger 클래스 추가. 모델 로딩, 토큰화, 추론 수행 및 상위 K개 감정 레이블 반환. 모듈 수준 singleton 인스턴스 제공.
Python API 엔드포인트 재설계
apps/machine/app/api/tagging.py
TaggingRequest/TaggingResponse 구조 변경 (media_id, mood_tags 기반). mood_tagger 통합, 표준화된 오류 처리, OpenAPI 스키마 개선.
오류 처리 인프라
apps/machine/app/errors.py
새로운 ErrorCode 열거형, AppException 클래스, 비동기 예외 핸들러 4개 추가. 일관된 오류 응답 형식 (success, code, message, status, timestamp, detail, errors) 제공.
Python 애플리케이션 설정
apps/machine/app/main.py, apps/machine/app/config.py, apps/machine/app/constants.py
FastAPI 앱 메타데이터 확장, 전역 예외 핸들러 등록, CORS 정책 업데이트, 감정 태그 상수 통합, NoDecode 환경 파싱 추가.
ML 의존성
apps/machine/requirements.txt
torch, transformers, numpy 라이브러리 추가.
Java 비동기 AI 태깅 서비스
apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java
새로운 @Async @Service 클래스. 미디어 ID와 설명을 받아 AI 태그 생성, 기존 MoodTag와 매핑, MediaMoodTag 엔티티 영속성 처리.
Java 도메인 엔티티 및 저장소
modules/domain/src/main/java/com/ott/domain/mood_tag/..., modules/domain/src/main/java/com/ott/domain/mood_category/..., modules/domain/src/main/java/com/ott/domain/media_mood_tag/...
MoodCategory, MoodTag, MediaMoodTag JPA 엔티티 추가. 각 엔티티에 해당하는 JpaRepository 인터페이스 추가.
데이터베이스 마이그레이션
modules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sql, modules/infra-db/src/main/resources/db/migration/V8__seed_mood_tags_and_add_priority.sql
mood_category, mood_tag, media_mood_tag 테이블 및 외래키 제약, 고유 인덱스 생성. 8개 카테고리, 31개 감정 태그 시드 데이터 추가. priority 컬럼 추가.
설정 및 무시 패턴
.gitignore
Python 캐시, ML 모델 체크포인트, Node.js 잠금 파일, 로컬 설정 파일 무시 패턴 추가.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as API Server
    participant AsyncService as AITaggingAsyncService
    participant MLService as MoodTagger
    participant Repository as Repository Layer
    participant Database as Database

    Client->>API: POST /tagging (media_id, description)
    API->>API: Validate Request
    API->>AsyncService: processAiTagging(mediaId, description)
    activate AsyncService
    AsyncService->>MLService: predict(description, top_k=3)
    activate MLService
    MLService->>MLService: Load Model & Tokenize
    MLService->>MLService: Run Inference (Sigmoid)
    MLService->>MLService: Select Top-K Labels
    MLService-->>AsyncService: mood_tags[list]
    deactivate MLService
    
    AsyncService->>Repository: findByNameInAndStatus(mood_tags)
    Repository->>Database: Query MoodTag
    Database-->>Repository: MoodTag Entities
    Repository-->>AsyncService: MoodTag List
    
    AsyncService->>Repository: deleteByMedia_Id(mediaId)
    Repository->>Database: Delete Existing Associations
    
    AsyncService->>Repository: save(MediaMoodTag List)
    Repository->>Database: Insert New Associations
    Database-->>Repository: Success
    Repository-->>AsyncService: Persisted Entities
    
    AsyncService-->>API: Completion
    deactivate AsyncService
    API-->>Client: 202 Accepted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • phonil
  • arlen02-01
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR title '[OTT-277][FEAT]: flyway 신규 버전 및 엔티티 추가' accurately describes the main changes: Flyway version upgrade and new entity additions.
Linked Issues check ✅ Passed The PR implements core requirements from issue #151: ML dataset support via mood tagging entities and schema, ML model inference integration via mood_tagger service, and API integration for tag processing via AITaggingAsyncService.
Out of Scope Changes check ✅ Passed All changes align with objectives: database schema (Flyway migration), domain entities, ML inference service, API endpoint updates, and configuration changes are all necessary for AI tagging functionality.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch OT-277-feature/tagging-ml

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.

Copy link
Copy Markdown

@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: 5

🧹 Nitpick comments (10)
modules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sql (1)

41-49: mood_tag_id 단일 컬럼 인덱스 추가 고려

현재 유니크 인덱스 uq_media_mood_tag_media_id_mood_tag_idmedia_id가 선행 컬럼이므로, mood_tag_id로만 필터링하는 쿼리(예: 특정 태그가 적용된 모든 미디어 조회)에서는 인덱스 활용이 제한적입니다. 역방향 조회가 빈번하다면 mood_tag_id 단일 인덱스 추가를 검토해 주세요.

📊 선택적 인덱스 추가 제안
 CREATE UNIQUE INDEX uq_media_mood_tag_media_id_mood_tag_id
       ON media_mood_tag (media_id, mood_tag_id);
+
+CREATE INDEX idx_media_mood_tag_mood_tag_id
+      ON media_mood_tag (mood_tag_id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sql`
around lines 41 - 49, Add a single-column index on media_mood_tag.mood_tag_id to
support queries that filter by mood_tag_id alone; locate the media_mood_tag
table (see existing unique index uq_media_mood_tag_media_id_mood_tag_id and FKs
fk_media_mood_tag_to_media / fk_media_mood_tag_to_mood_tag) and create a
non-unique index like idx_media_mood_tag_mood_tag_id on mood_tag_id to improve
reverse-lookups of all media for a given mood tag.
modules/domain/src/main/java/com/ott/domain/mood_tag/repository/MoodTagRepository.java (1)

11-11: 컬렉션 파라미터 네이밍 컨벤션

코딩 가이드라인에 따르면 컬렉션 변수명에는 List 접미사를 사용해야 합니다. aiTagsaiTagNameList 또는 nameList로 변경을 권장합니다.

♻️ 네이밍 수정 제안
-    List<MoodTag> findByNameInAndStatus(List<String> aiTags, Status status);
+    List<MoodTag> findByNameInAndStatus(List<String> nameList, Status status);

As per coding guidelines, "Collection variable names with List suffix".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/domain/src/main/java/com/ott/domain/mood_tag/repository/MoodTagRepository.java`
at line 11, Rename the collection parameter in the repository method to follow
the "List" suffix convention: update the signature of MoodTagRepository's
findByNameInAndStatus to use a clearer name such as aiTagNameList or nameList
(i.e., change List<String> aiTags to List<String> aiTagNameList) and update all
call sites and usages accordingly so parameter naming remains consistent with
the coding guideline.
modules/domain/src/main/java/com/ott/domain/mood_category/repository/MoodCategoryRepository.java (1)

3-8: 사용되지 않는 import 제거

Status, List, Optional이 import되었으나 인터페이스 내에서 사용되지 않습니다.

♻️ 불필요한 import 제거
 package com.ott.domain.mood_category.repository;
 
-import com.ott.domain.common.Status;
 import com.ott.domain.mood_category.domain.MoodCategory;
 import org.springframework.data.jpa.repository.JpaRepository;
 
-import java.util.List;
-import java.util.Optional;
-
 public interface MoodCategoryRepository extends JpaRepository<MoodCategory, Long> {
 
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/domain/src/main/java/com/ott/domain/mood_category/repository/MoodCategoryRepository.java`
around lines 3 - 8, Remove the unused imports in the MoodCategoryRepository
interface: delete the imported symbols Status, List, and Optional from the
import block so only actual dependencies (e.g., MoodCategory and JpaRepository)
remain; update the import section in the MoodCategoryRepository file to reflect
only the types referenced by the interface.
apps/machine/requirements.txt (1)

6-8: ML 의존성 버전 고정 필요

torch, transformers, numpy에 버전이 명시되지 않아 빌드 재현성 문제가 발생할 수 있습니다. PyTorch와 Transformers는 메이저 버전 간 호환성 변경이 잦아 버전 고정이 중요합니다.

♻️ 버전 고정 예시
-torch
-transformers
-numpy
+torch>=2.1.0,<3.0.0
+transformers>=4.35.0,<6.0.0
+numpy>=1.24.0,<2.5.0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/machine/requirements.txt` around lines 6 - 8, requirements.txt에 torch,
transformers, numpy가 버전 미지정으로 적혀 있어 빌드 재현성이 깨집니다; requirements.txt에서 각각의
패키지(torch, transformers, numpy)를 명시적 버전(예: == 또는 ~= 등 재현성을 보장하는 연산자)으로 고정하고,
PyTorch-Transformers 상호 호환성을 확인해 호환 가능한 메이저/마이너 조합을 선택하며(또는 주석으로 호환성 근거를 남김)
필요하면 별도의 constraints 파일로 관리하도록 변경하세요.
apps/machine/app/main.py (1)

63-69: Health 체크 엔드포인트 개선 고려

현재 health check는 단순히 {"status": "ok"}를 반환합니다. ML 모델 로딩 상태를 포함하면 운영 모니터링에 더 유용할 수 있습니다.

♻️ 개선 예시
from app.services.tagging import mood_tagger

`@app.get`("/health")
def health() -> dict[str, str]:
    model_loaded = mood_tagger.model is not None
    return {
        "status": "ok" if model_loaded else "degraded",
        "model_loaded": model_loaded,
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/machine/app/main.py` around lines 63 - 69, The /health endpoint
currently returns a static {"status":"ok"}; update the health() function to
check the ML model load state (use the mood_tagger object from
app.services.tagging, e.g., mood_tagger.model) and return a JSON that includes
model_loaded (boolean) and set status to "ok" when model_loaded is true and
"degraded" when false; ensure you import mood_tagger at top and keep the
endpoint signature health() -> dict[str, Any] (or dict[str, str|bool]) to
reflect the added field.
apps/machine/app/services/tagging.py (1)

27-28: 예외 로깅 개선: logger.exception() 사용 권장

정적 분석 도구(Ruff TRY400)에서 지적한 대로, 예외 발생 시 스택 트레이스를 자동으로 포함하려면 logger.exception()을 사용하는 것이 좋습니다.

♻️ 제안된 수정사항
         except Exception as e:
-            logger.error(f"모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요: {e}")
+            logger.exception("모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요.")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/machine/app/services/tagging.py` around lines 27 - 28, Replace the
current exception log call that uses logger.error in the except Exception as e
block with logger.exception so the stack trace is recorded; locate the except
Exception as e handler around the model-loading logic (the block that now logs
"모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요: {e}") and change the call to logger.exception
with the same descriptive message to capture the traceback.
apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java (1)

53-66: 코딩 가이드라인: 컬렉션 변수명에 List 접미사 사용 권장

코딩 가이드라인에 따라 컬렉션 변수명에 List 접미사를 사용해야 합니다.

♻️ 제안된 수정사항
-            List<MoodTag> foundMoodTags = moodTagRepository.findByNameInAndStatus(aiTags, Status.ACTIVE);
-            Map<String, MoodTag> moodTagByName = foundMoodTags.stream()
+            List<MoodTag> foundMoodTagList = moodTagRepository.findByNameInAndStatus(aiTags, Status.ACTIVE);
+            Map<String, MoodTag> moodTagByName = foundMoodTagList.stream()
                     .collect(Collectors.toMap(MoodTag::getName, Function.identity(), (left, right) -> left, LinkedHashMap::new));

-            List<MediaMoodTag> newMediaMoodTags = IntStream.range(0, aiTags.size())
+            List<MediaMoodTag> newMediaMoodTagList = IntStream.range(0, aiTags.size())

마찬가지로 aiTagsaiTagList, missingTagsmissingTagList로 변경을 고려하세요.

As per coding guidelines, "Collection variable names with List suffix".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`
around lines 53 - 66, Rename collection variables to follow the `List` suffix
guideline: replace `aiTags` with `aiTagList` (and `missingTags` with
`missingTagList` where used) throughout the AITaggingAsyncService class/method
so all references (calls to
moodTagRepository.findByNameInAndStatus(aiTagList,...), the IntStream.range
mapping, filters using aiTagList.indexOf(...), and creation of newMediaMoodTags
via MediaMoodTag.builder()) are updated consistently; ensure local variables
foundMoodTags, moodTagByName and newMediaMoodTags remain unchanged but consume
the renamed aiTagList variable and update any parameter names/signatures that
pass the collection to other methods.
apps/machine/app/api/tagging.py (2)

11-24: ErrorItemErrorResponse 모델 중복 가능성

errors.py에서 이미 유사한 에러 응답 구조를 정의하고 있습니다. OpenAPI 문서화를 위해 별도로 정의한 것일 수 있으나, 공통 모델로 통합하여 일관성을 유지하는 것을 고려하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/machine/app/api/tagging.py` around lines 11 - 24, ErrorItem and
ErrorResponse are duplicated here; replace these local definitions with the
shared models from the common errors module: remove the ErrorItem and
ErrorResponse classes in this file and import the canonical ErrorItem and
ErrorResponse from the existing errors.py (preserving Field
descriptions/examples there or augmenting them if needed), then update any
references in functions/route decorators in this module to use the imported
ErrorItem/ErrorResponse symbols so OpenAPI docs remain identical.

143-147: 예외 체인 보존 및 로깅 개선

정적 분석 도구(Ruff B904)에서 지적한 대로, 예외를 다시 발생시킬 때 원본 예외를 연결하면 디버깅이 용이합니다. 또한 프로덕션 환경에서 str(e)를 응답에 포함하면 민감한 정보가 노출될 수 있습니다.

♻️ 제안된 수정사항
     except AppException:
         raise
     except Exception as e:
+        # 프로덕션에서는 상세 에러 메시지 노출 주의
         raise AppException(
             ErrorCode.INTERNAL_ERROR,
-            detail=f"태깅 처리 중 예외가 발생했습니다: {str(e)}",
-        )
+            detail="태깅 처리 중 예외가 발생했습니다",
+        ) from e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/machine/app/api/tagging.py` around lines 143 - 147, Replace the current
except block that raises AppException with a pattern that preserves the original
exception chain and avoids exposing exception text in the API response: log the
original exception (including stack trace) using the module logger inside the
except handler, then raise AppException(ErrorCode.INTERNAL_ERROR, detail="태깅 처리
중 예외가 발생했습니다") from e so the original exception is chained but the response
detail omits sensitive str(e) content; reference the existing AppException and
ErrorCode.INTERNAL_ERROR symbols and the except handler around the tagging
logic.
apps/machine/app/errors.py (1)

93-93: 타임스탬프에 타임존 정보 포함 권장

datetime.now().isoformat()은 타임존 정보가 없는 로컬 시간을 반환합니다. 분산 시스템에서 로그 분석 시 혼란을 방지하려면 UTC 또는 타임존이 포함된 시간을 사용하는 것이 좋습니다.

♻️ 제안된 수정사항
-from datetime import datetime
+from datetime import datetime, timezone

# _error_body 내부
-        "timestamp": datetime.now().isoformat(),
+        "timestamp": datetime.now(timezone.utc).isoformat(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/machine/app/errors.py` at line 93, The "timestamp" entry currently uses
datetime.now().isoformat() which lacks timezone info; update the timestamp
generation in errors.py to produce an ISO8601 timestamp with timezone (e.g., use
datetime.now(timezone.utc).isoformat() or equivalent) and ensure the
datetime.timezone (or timezone import) is present so logs carry UTC/tz info;
adjust the code that builds the "timestamp" value accordingly (reference the
"timestamp" key and the use of datetime.now().isoformat()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 82-83: In AITaggingAsyncService where you call
mediaMoodTagRepository.deleteByMedia_Id(mediaId) followed by
mediaMoodTagRepository.saveAll(newMediaMoodTags), wrap the operation in a
`@Transactional` method and enforce a DB-level lock or optimistic versioning to
prevent concurrent-modify races; e.g., obtain the Media row with
MediaRepository.findById(mediaId) using `@Lock`(LockModeType.PESSIMISTIC_WRITE)
(or add an `@Version` field to the Media/MediaMoodTag entity and handle
OptimisticLockingFailureException) and perform the delete+save inside that
transaction so concurrent requests for the same mediaId are serialized or will
retry on version conflict.
- Around line 86-90: In AITaggingAsyncService update the `@Async` void method that
currently catches BusinessException and rethrows it so that it no longer
rethrows; instead handle BusinessException the same way as the general Exception
branch by logging it with the same log.error call (include mediaId and the
exception), removing the throw e; statement; ensure the catch blocks are
consolidated or both call the same logger to provide consistent logging for any
exception thrown in the async method.

In `@apps/machine/app/errors.py`:
- Around line 158-161: The unhandled_exception_handler currently returns the
exception text via content=_error_body(..., detail=str(exc)) which may expose
internal details; change it to return a generic error detail (e.g., a fixed
message like "Internal server error") while logging the real exception
server-side using the application's logger (call logger.exception or
logger.error with the exception) before returning; update the call in
unhandled_exception_handler to pass the generic detail to _error_body and ensure
the real exc is logged for diagnostics.

In `@apps/machine/app/main.py`:
- Around line 43-50: The CORS middleware is currently configured with
allow_origins=["*"] which is unsafe for production; replace the hardcoded
wildcard in the app.add_middleware(CORSMiddleware, ...) call by loading allowed
origins from configuration or environment (e.g. settings.cors_origins) and pass
that value to allow_origins, keeping
allow_credentials/allow_methods/allow_headers as configured; ensure the settings
value is validated (list of origins) and falls back to a safe default for dev.

In
`@modules/infra-db/src/main/resources/db/migration/V8__seed_mood_tags_and_add_priority.sql`:
- Around line 4-17: The INSERT statement using deprecated VALUES() should be
changed to use a row alias: update the INSERT INTO mood_category (...) VALUES
(...) block to add "AS new" immediately after the VALUES list and replace usages
of VALUES(name)/VALUES(status) in the ON DUPLICATE KEY UPDATE clause with
new.name and new.status, keeping modified_date = NOW() as-is; locate the INSERT
INTO mood_category statement and the ON DUPLICATE KEY UPDATE clause to apply
this change.

---

Nitpick comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 53-66: Rename collection variables to follow the `List` suffix
guideline: replace `aiTags` with `aiTagList` (and `missingTags` with
`missingTagList` where used) throughout the AITaggingAsyncService class/method
so all references (calls to
moodTagRepository.findByNameInAndStatus(aiTagList,...), the IntStream.range
mapping, filters using aiTagList.indexOf(...), and creation of newMediaMoodTags
via MediaMoodTag.builder()) are updated consistently; ensure local variables
foundMoodTags, moodTagByName and newMediaMoodTags remain unchanged but consume
the renamed aiTagList variable and update any parameter names/signatures that
pass the collection to other methods.

In `@apps/machine/app/api/tagging.py`:
- Around line 11-24: ErrorItem and ErrorResponse are duplicated here; replace
these local definitions with the shared models from the common errors module:
remove the ErrorItem and ErrorResponse classes in this file and import the
canonical ErrorItem and ErrorResponse from the existing errors.py (preserving
Field descriptions/examples there or augmenting them if needed), then update any
references in functions/route decorators in this module to use the imported
ErrorItem/ErrorResponse symbols so OpenAPI docs remain identical.
- Around line 143-147: Replace the current except block that raises AppException
with a pattern that preserves the original exception chain and avoids exposing
exception text in the API response: log the original exception (including stack
trace) using the module logger inside the except handler, then raise
AppException(ErrorCode.INTERNAL_ERROR, detail="태깅 처리 중 예외가 발생했습니다") from e so
the original exception is chained but the response detail omits sensitive str(e)
content; reference the existing AppException and ErrorCode.INTERNAL_ERROR
symbols and the except handler around the tagging logic.

In `@apps/machine/app/errors.py`:
- Line 93: The "timestamp" entry currently uses datetime.now().isoformat() which
lacks timezone info; update the timestamp generation in errors.py to produce an
ISO8601 timestamp with timezone (e.g., use
datetime.now(timezone.utc).isoformat() or equivalent) and ensure the
datetime.timezone (or timezone import) is present so logs carry UTC/tz info;
adjust the code that builds the "timestamp" value accordingly (reference the
"timestamp" key and the use of datetime.now().isoformat()).

In `@apps/machine/app/main.py`:
- Around line 63-69: The /health endpoint currently returns a static
{"status":"ok"}; update the health() function to check the ML model load state
(use the mood_tagger object from app.services.tagging, e.g., mood_tagger.model)
and return a JSON that includes model_loaded (boolean) and set status to "ok"
when model_loaded is true and "degraded" when false; ensure you import
mood_tagger at top and keep the endpoint signature health() -> dict[str, Any]
(or dict[str, str|bool]) to reflect the added field.

In `@apps/machine/app/services/tagging.py`:
- Around line 27-28: Replace the current exception log call that uses
logger.error in the except Exception as e block with logger.exception so the
stack trace is recorded; locate the except Exception as e handler around the
model-loading logic (the block that now logs "모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요:
{e}") and change the call to logger.exception with the same descriptive message
to capture the traceback.

In `@apps/machine/requirements.txt`:
- Around line 6-8: requirements.txt에 torch, transformers, numpy가 버전 미지정으로 적혀 있어
빌드 재현성이 깨집니다; requirements.txt에서 각각의 패키지(torch, transformers, numpy)를 명시적 버전(예:
== 또는 ~= 등 재현성을 보장하는 연산자)으로 고정하고, PyTorch-Transformers 상호 호환성을 확인해 호환 가능한
메이저/마이너 조합을 선택하며(또는 주석으로 호환성 근거를 남김) 필요하면 별도의 constraints 파일로 관리하도록 변경하세요.

In
`@modules/domain/src/main/java/com/ott/domain/mood_category/repository/MoodCategoryRepository.java`:
- Around line 3-8: Remove the unused imports in the MoodCategoryRepository
interface: delete the imported symbols Status, List, and Optional from the
import block so only actual dependencies (e.g., MoodCategory and JpaRepository)
remain; update the import section in the MoodCategoryRepository file to reflect
only the types referenced by the interface.

In
`@modules/domain/src/main/java/com/ott/domain/mood_tag/repository/MoodTagRepository.java`:
- Line 11: Rename the collection parameter in the repository method to follow
the "List" suffix convention: update the signature of MoodTagRepository's
findByNameInAndStatus to use a clearer name such as aiTagNameList or nameList
(i.e., change List<String> aiTags to List<String> aiTagNameList) and update all
call sites and usages accordingly so parameter naming remains consistent with
the coding guideline.

In
`@modules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sql`:
- Around line 41-49: Add a single-column index on media_mood_tag.mood_tag_id to
support queries that filter by mood_tag_id alone; locate the media_mood_tag
table (see existing unique index uq_media_mood_tag_media_id_mood_tag_id and FKs
fk_media_mood_tag_to_media / fk_media_mood_tag_to_mood_tag) and create a
non-unique index like idx_media_mood_tag_mood_tag_id on mood_tag_id to improve
reverse-lookups of all media for a given mood tag.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ca50f983-3bb8-4f49-b4d2-5f52016eb516

📥 Commits

Reviewing files that changed from the base of the PR and between a4be9ff and f8eb6e4.

📒 Files selected for processing (18)
  • .gitignore
  • apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java
  • apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AiTaggingAsyncService.java
  • apps/machine/app/api/tagging.py
  • apps/machine/app/config.py
  • apps/machine/app/constants.py
  • apps/machine/app/errors.py
  • apps/machine/app/main.py
  • apps/machine/app/services/tagging.py
  • apps/machine/requirements.txt
  • modules/domain/src/main/java/com/ott/domain/media_mood_tag/domain/MediaMoodTag.java
  • modules/domain/src/main/java/com/ott/domain/media_mood_tag/repository/MediaMoodTagRepository.java
  • modules/domain/src/main/java/com/ott/domain/mood_category/domain/MoodCategory.java
  • modules/domain/src/main/java/com/ott/domain/mood_category/repository/MoodCategoryRepository.java
  • modules/domain/src/main/java/com/ott/domain/mood_tag/domain/MoodTag.java
  • modules/domain/src/main/java/com/ott/domain/mood_tag/repository/MoodTagRepository.java
  • modules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sql
  • modules/infra-db/src/main/resources/db/migration/V8__seed_mood_tags_and_add_priority.sql
💤 Files with no reviewable changes (1)
  • apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AiTaggingAsyncService.java

Comment on lines +82 to +83
mediaMoodTagRepository.deleteByMedia_Id(mediaId);
mediaMoodTagRepository.saveAll(newMediaMoodTags);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

동시성 문제: 동일 mediaId에 대한 동시 호출 시 데이터 불일치 가능성

deleteByMedia_IdsaveAll 사이에 동일한 mediaId로 다른 요청이 들어오면 데이터 일관성 문제가 발생할 수 있습니다. 동시 업로드가 드물다면 현재 구현도 괜찮지만, 빈번한 경우 비관적 잠금 또는 @Version을 고려하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`
around lines 82 - 83, In AITaggingAsyncService where you call
mediaMoodTagRepository.deleteByMedia_Id(mediaId) followed by
mediaMoodTagRepository.saveAll(newMediaMoodTags), wrap the operation in a
`@Transactional` method and enforce a DB-level lock or optimistic versioning to
prevent concurrent-modify races; e.g., obtain the Media row with
MediaRepository.findById(mediaId) using `@Lock`(LockModeType.PESSIMISTIC_WRITE)
(or add an `@Version` field to the Media/MediaMoodTag entity and handle
OptimisticLockingFailureException) and perform the delete+save inside that
transaction so concurrent requests for the same mediaId are serialized or will
retry on version conflict.

Comment on lines +86 to +90
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# AsyncUncaughtExceptionHandler 설정 여부 확인
rg -n "AsyncUncaughtExceptionHandler|AsyncConfigurer" --type=java

Repository: OpenTheTaste/backend

Length of output: 46


🏁 Script executed:

fd "AITaggingAsyncService.java" -t f --exec cat -n {} \;

Repository: OpenTheTaste/backend

Length of output: 4540


🏁 Script executed:

rg -n "@Async" --type=java -A 5 | head -100

Repository: OpenTheTaste/backend

Length of output: 867


🏁 Script executed:

rg -n "class AITaggingAsyncService" --type=java -A 50 | head -80

Repository: OpenTheTaste/backend

Length of output: 7174


🏁 Script executed:

# Check if there are any `@Transactional` `@Async` combinations
rg -n "@Transactional" --type=java -B 2 | grep -A 2 "@Async"

Repository: OpenTheTaste/backend

Length of output: 46


🏁 Script executed:

# Search for AsyncConfigurer or any Spring config related to async
fd ".*Config.*" --type=f -name "*.java" | head -20 | xargs rg -l "async|Async" 2>/dev/null

Repository: OpenTheTaste/backend

Length of output: 428


🏁 Script executed:

# Look for configuration files that might set up async
fd ".*Config\.java" --type=f | xargs rg -l "async|Async|@EnableAsync" 2>/dev/null

Repository: OpenTheTaste/backend

Length of output: 48


🏁 Script executed:

# Check the specific `@Transactional` `@Async` in AITaggingAsyncService
sed -n '36,42p' apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java

Repository: OpenTheTaste/backend

Length of output: 292


🏁 Script executed:

# Verify BusinessException behavior - check how it's defined
rg -n "class BusinessException" --type=java -A 10

Repository: OpenTheTaste/backend

Length of output: 1352


@Async void 메서드에서 예외 rethrow는 효과 없음

@Async 메서드의 반환 타입이 void이므로, 메서드 내에서 throw한 BusinessException은 호출자에게 도달하지 않습니다. 비동기 스레드에서 발생한 예외는 AsyncUncaughtExceptionHandler가 설정되지 않은 경우 일관된 처리가 불가능합니다.

현재 코드는 BusinessException을 분리하여 rethrow하지만, 이는 실제로 에러 처리 관점에서 의미가 없으며, 일반 Exception 처리(로깅)와도 불일치합니다. 모든 예외를 일관되게 로깅으로 처리하세요.

제안된 수정사항
-        } catch (BusinessException e) {
-            throw e;
         } catch (Exception e) {
             log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e);
         }
📝 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
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e);
}
} catch (Exception e) {
log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`
around lines 86 - 90, In AITaggingAsyncService update the `@Async` void method
that currently catches BusinessException and rethrows it so that it no longer
rethrows; instead handle BusinessException the same way as the general Exception
branch by logging it with the same log.error call (include mediaId and the
exception), removing the throw e; statement; ensure the catch blocks are
consolidated or both call the same logger to provide consistent logging for any
exception thrown in the async method.

Comment on lines +158 to +161
async def unhandled_exception_handler(_: Request, exc: Exception) -> JSONResponse:
return JSONResponse(
status_code=ErrorCode.INTERNAL_ERROR.http_status,
content=_error_body(ErrorCode.INTERNAL_ERROR, detail=str(exc)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

처리되지 않은 예외의 상세 정보 노출 주의

str(exc)를 응답에 포함하면 스택 트레이스나 내부 구현 세부사항이 노출될 수 있습니다. 프로덕션에서는 일반적인 에러 메시지만 반환하고, 상세 내용은 서버 로그에만 기록하는 것이 안전합니다.

🛠️ 제안된 수정사항
+import logging
+
+logger = logging.getLogger(__name__)
+
 async def unhandled_exception_handler(_: Request, exc: Exception) -> JSONResponse:
+    logger.exception("Unhandled exception occurred")
     return JSONResponse(
         status_code=ErrorCode.INTERNAL_ERROR.http_status,
-        content=_error_body(ErrorCode.INTERNAL_ERROR, detail=str(exc)),
+        content=_error_body(ErrorCode.INTERNAL_ERROR),
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/machine/app/errors.py` around lines 158 - 161, The
unhandled_exception_handler currently returns the exception text via
content=_error_body(..., detail=str(exc)) which may expose internal details;
change it to return a generic error detail (e.g., a fixed message like "Internal
server error") while logging the real exception server-side using the
application's logger (call logger.exception or logger.error with the exception)
before returning; update the call in unhandled_exception_handler to pass the
generic detail to _error_body and ensure the real exc is logged for diagnostics.

Comment thread apps/machine/app/main.py
Comment on lines +43 to 50
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_allow_origins,
allow_origins=["*"], # 운영에서는 프론트 도메인만 허용 권장
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

프로덕션 CORS 설정 확인 필요

주석에서 언급한 대로 allow_origins=["*"]는 개발 환경에 적합하지만 프로덕션에서는 보안상 특정 도메인만 허용해야 합니다. 배포 전 환경 변수 또는 설정 파일을 통해 허용 도메인을 관리하는 것을 권장합니다.

🛠️ 환경별 설정 예시
from app.config import settings

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.cors_origins,  # 환경별 설정
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/machine/app/main.py` around lines 43 - 50, The CORS middleware is
currently configured with allow_origins=["*"] which is unsafe for production;
replace the hardcoded wildcard in the app.add_middleware(CORSMiddleware, ...)
call by loading allowed origins from configuration or environment (e.g.
settings.cors_origins) and pass that value to allow_origins, keeping
allow_credentials/allow_methods/allow_headers as configured; ensure the settings
value is validated (list of origins) and falls back to a safe default for dev.

Comment on lines +4 to +17
INSERT INTO mood_category (id, name, created_date, modified_date, status)
VALUES
(1, '슬픔', NOW(), NOW(), 'ACTIVE'),
(2, '공포', NOW(), NOW(), 'ACTIVE'),
(3, '유쾌', NOW(), NOW(), 'ACTIVE'),
(4, '힐링', NOW(), NOW(), 'ACTIVE'),
(5, '설렘', NOW(), NOW(), 'ACTIVE'),
(6, '지식', NOW(), NOW(), 'ACTIVE'),
(7, '자극', NOW(), NOW(), 'ACTIVE'),
(8, '도파민', NOW(), NOW(), 'ACTIVE')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
modified_date = NOW(),
status = VALUES(status);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

MySQL 8.0 VALUES() deprecated INSERT ON DUPLICATE KEY UPDATE

💡 Result:

In MySQL 8.0.20, using VALUES(col) inside INSERT ... ON DUPLICATE KEY UPDATE was deprecated (it still works, but raises a deprecation warning and may be removed in a future release). (dev.mysql.com)

Replace VALUES() with a row (and optional column) alias

1) INSERT ... VALUES ... ON DUPLICATE KEY UPDATE

Old (deprecated):

INSERT INTO t1 (a,b,c) VALUES (1,2,3),(4,5,6)
ON DUPLICATE KEY UPDATE c = VALUES(a) + VALUES(b);

New (recommended):

INSERT INTO t1 (a,b,c) VALUES (1,2,3),(4,5,6) AS new
ON DUPLICATE KEY UPDATE c = new.a + new.b;

You can also alias the inserted columns:

INSERT INTO t1 (a,b,c) VALUES (1,2,3),(4,5,6) AS new(m,n,p)
ON DUPLICATE KEY UPDATE c = m + n;
``` ([dev.mysql.com](https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-20.html))

### 2) `INSERT ... SET ... ON DUPLICATE KEY UPDATE`
```sql
INSERT INTO t1 SET a=1,b=2,c=3 AS new
ON DUPLICATE KEY UPDATE c = new.a + new.b;
``` ([dev.mysql.com](https://dev.mysql.com/doc/refman/8.3/en/insert-on-duplicate.html))

### 3) `INSERT ... SELECT ... ON DUPLICATE KEY UPDATE`
If you used `VALUES()` here, MySQL warns; the manual shows eliminating it by wrapping the `SELECT` as a derived table and referencing its output column:
```sql
INSERT INTO t1
  SELECT * FROM (SELECT c, c+d AS e FROM t2) AS dt
ON DUPLICATE KEY UPDATE b = e;
``` ([dev.mysql.com](https://dev.mysql.com/doc/refman/8.3/en/insert-on-duplicate.html))

Citations:

- 1: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-20.html
- 2: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-20.html
- 3: https://dev.mysql.com/doc/refman/8.3/en/insert-on-duplicate.html
- 4: https://dev.mysql.com/doc/refman/8.3/en/insert-on-duplicate.html

---



</details>

**`VALUES()` 함수를 새로운 alias 구문으로 변경 필요**

`VALUES()` 구문은 MySQL 8.0.20부터 deprecated되었습니다. 다음과 같이 행 alias를 사용하여 변경하세요:

<details>
<summary>수정된 SQL</summary>

```sql
INSERT INTO mood_category (id, name, created_date, modified_date, status)
VALUES
    (1, '슬픔', NOW(), NOW(), 'ACTIVE'),
    (2, '공포', NOW(), NOW(), 'ACTIVE'),
    (3, '유쾌', NOW(), NOW(), 'ACTIVE'),
    (4, '힐링', NOW(), NOW(), 'ACTIVE'),
    (5, '설렘', NOW(), NOW(), 'ACTIVE'),
    (6, '지식', NOW(), NOW(), 'ACTIVE'),
    (7, '자극', NOW(), NOW(), 'ACTIVE'),
    (8, '도파민', NOW(), NOW(), 'ACTIVE')
AS new
ON DUPLICATE KEY UPDATE
    name = new.name,
    modified_date = NOW(),
    status = new.status;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/infra-db/src/main/resources/db/migration/V8__seed_mood_tags_and_add_priority.sql`
around lines 4 - 17, The INSERT statement using deprecated VALUES() should be
changed to use a row alias: update the INSERT INTO mood_category (...) VALUES
(...) block to add "AS new" immediately after the VALUES list and replace usages
of VALUES(name)/VALUES(status) in the ON DUPLICATE KEY UPDATE clause with
new.name and new.status, keeping modified_date = NOW() as-is; locate the INSERT
INTO mood_category statement and the ON DUPLICATE KEY UPDATE clause to apply
this change.

@yubin012 yubin012 merged commit 15c8b02 into develop Mar 12, 2026
1 check passed
@phonil phonil deleted the OT-277-feature/tagging-ml branch April 4, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 새로운 기능 구현 fix 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[OT-277] [FEAT]: AI 태그 라벨링 및 학습 API 연동

2 participants