[Feat] 영상 분석 파이프라인 견고화 및 미처리 영상 자동 분석 도입#66
Conversation
📝 WalkthroughWalkthrough비디오 분석 파이프라인에 요청 출처(Source) 추적을 도입하고, 우선순위 기반 큐와 프로세스(프록시 사용자명 로테이션, 트랜스크립트 클라이언트, 배치 백필 스케줄러)를 추가하며 HTTP API 응답 계약과 관련 설정/환경 변수를 업데이트했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Scheduler as VideoAnalysisBackfillScheduler
participant VideoPersistence as YouTubeVideoPersistencePort
participant VideoAnalyze as VideoAnalyzeUseCase
participant Queue as VideoAnalysisQueuePort
Scheduler->>Scheduler: run() (cron)
Scheduler->>VideoPersistence: findUnanalyzedVideoIds(limit=5)
VideoPersistence-->>Scheduler: List<videoId>
alt 발견됨
loop 각 videoId
Scheduler->>VideoAnalyze: analyzeVideo(youtubeUrl, source=BATCH)
VideoAnalyze->>Queue: enqueue(taskId, youtubeUrl, BATCH)
Queue-->>VideoAnalyze: queued
end
else 없음
Scheduler->>Scheduler: 로그: 미처리 영상 없음
end
sequenceDiagram
participant Client as ProxyYoutubeClient
participant Proxies as ProxyList
participant YouTube as YouTubeServer
Client->>Client: get(url)
Client->>Client: executeWithRotation(request)
loop 각 proxy 사용자명
Client->>Proxies: 선택된 username
Client->>YouTube: 요청(프록시 사용)
alt 2xx
YouTube-->>Client: 성공
Client-->>Client: 반환
else 429/403
YouTube-->>Client: 차단
Client->>Client: 기록 후 다음 proxy 시도
else 5xx/기타 오류
YouTube-->>Client: 서버 오류
Client-->>Client: 즉시 예외 던짐
end
end
alt 모두 소진
Client-->>Client: 차단/네트워크 실패 요약으로 예외 던짐
end
sequenceDiagram
participant Listener as VideoAnalyzeEventListener
participant QueuePort as VideoAnalysisQueuePort
participant InMem as InMemoryVideoAnalysisQueueAdapter
participant PQ as PriorityBlockingQueue
Listener->>Listener: onEvent(event with source)
Listener->>QueuePort: enqueue(id, url, source)
QueuePort->>InMem: enqueue(id, url, source)
InMem->>InMem: QueueEntry(event, seq++)
InMem->>PQ: offer(QueueEntry)
PQ-->>InMem: 저장(우선순위 기반)
InMem-->>QueuePort: 성공
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
linktrip-bootstrap/src/main/resources/application.yml (1)
47-48: sentinel-video-id를 환경변수로 오버라이드 가능하게 노출 권장
TVM6Nswlfbg가 하드코딩되어 있어 해당 영상이 비공개/삭제되거나 지역 차단될 경우 프록시 헬스체크(isProxyHealthy())가 영구적으로 실패 판정을 내려 모든 모호 실패가 PENDING으로 오분류될 수 있습니다. 환경변수 기본값 패턴으로 두고 운영 중 교체 가능하게 하는 것을 권장합니다.♻️ 제안 변경
health-check: - sentinel-video-id: TVM6Nswlfbg + sentinel-video-id: ${YOUTUBE_HEALTH_SENTINEL_VIDEO_ID:TVM6Nswlfbg}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-bootstrap/src/main/resources/application.yml` around lines 47 - 48, The hardcoded sentinel video id in the configuration (health-check.sentinel-video-id) should be made configurable via an environment variable with a default to avoid permanent failures if that video is removed or region-blocked; change the YAML to read the value from an env var (e.g. ${SENTINEL_VIDEO_ID:TVM6Nswlfbg}) and ensure any code referencing the key (e.g. isProxyHealthy()) uses the injected value so operators can override SENTINEL_VIDEO_ID in runtime environments.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt (1)
47-47: 운영 팁: anti-join 쿼리에 인덱스 확인 권장
video_analysis_task가 누적되면youtube_video와의 anti-join 성능이 저하될 수 있습니다.video_analysis_task.youtube_video_id(또는 매칭 기준 컬럼)와youtube_video측 ORDER BY 정렬 컬럼(생성일 기준일 경우created_at)에 인덱스가 존재하는지, 그리고 실제 EXPLAIN 상 인덱스 기반 NOT EXISTS/LEFT JOIN 형태로 풀리는지 확인을 권장합니다. 10분 주기 × 5건 소비 속도 대비 미분석 영상 증가 속도가 빠를 경우, limit 조정이나 주기 단축도 함께 고려해볼 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt` at line 47, findUnanalyzedVideoIds 구현에서 video_analysis_task와 youtube_video를 anti-join으로 조회할 때 인덱스/쿼리 플랜을 확인하도록 수정하세요: querydslRepository.findUnanalyzedVideoIds 호출에 사용되는 조인 키(video_analysis_task.youtube_video_id 또는 실제 매칭 컬럼)와 youtube_video의 정렬 컬럼(예: created_at)에 적절한 인덱스가 존재하는지 DB에 생성하고 EXPLAIN으로 NOT EXISTS/LEFT JOIN이 인덱스를 사용하는지 검증하며, 필요하면 인덱스 추가 또는 limit/스케줄링(주기 단축) 조정 로직을 논의하도록 코멘트를 남기세요.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt (1)
95-105: 대규모 데이터셋에서 정렬/조인 비용에 대한 인덱스 검토 권장쿼리는
youtube_video를created_at ASC로 정렬한 뒤video_analysis_task.youtube_url에 대해 anti-join 하는 구조입니다. 데이터가 쌓이면 다음 두 인덱스의 존재 여부를 확인해 주세요.
youtube_video(created_at)— ORDER BY + LIMIT 가 인덱스 스캔으로 풀리게.video_analysis_task(youtube_url)— LEFT JOIN 조건에서 seek 가 가능하게(이미uk_video_analysis_task_youtube_url유니크 제약이 있으므로 충족).둘 다 있다면 LIMIT 5 기준으로 매우 저렴하게 동작하고, 없다면 정렬 cost가 누적됩니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt` around lines 95 - 105, The query in findUnanalyzedVideoIds performs ORDER BY video.createdAt ASC with a LEFT JOIN on analysisTask.youtubeUrl, which will become expensive at scale; ensure the DB has an index on youtube_video(created_at) to allow index-ordered LIMIT scans and an index on video_analysis_task(youtube_url) (the unique constraint uk_video_analysis_task_youtube_url already satisfies this) so the LEFT JOIN/anti-join can seek efficiently; add or confirm those indexes at the database level and document/verify with EXPLAIN that the plan uses the indexes for LIMIT performance.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt (1)
96-109:LinktripException생성자가 원인 예외 체이닝을 지원하지 않음.
logger.warn(cause)로 스택 추적 정보는 남지만,LinktripException의 현재 생성자는cause파라미터를 받지 않습니다. ExceptionHandler 나 상위 로거에서 root cause (TranscriptRetrievalException) 를 추적하려면 생성자를 확장해야 합니다.현재 코드의 logging-then-throw 패턴은 스택을 보존하는 workaround 이지만, 구조적으로 정확한 예외 체이닝을 원한다면
LinktripException이 원인 예외를 받는 생성자를 제공해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt` around lines 96 - 109, Add a constructor to LinktripException that accepts a cause (Throwable) and wires it into the super/exception chain, then update classifyAmbiguousFailure to pass the original TranscriptRetrievalException as the cause when throwing LinktripException (i.e., replace the current throw LinktripException(ExceptionCode.BAD_GATEWAY_YOUTUBE, "…") with the new constructor that includes the cause). Ensure the new LinktripException constructor preserves message, code (ExceptionCode.BAD_GATEWAY_YOUTUBE) and sets the cause so exception handlers can access the root TranscriptRetrievalException.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeService.kt`:
- Around line 20-27: When reactivating a FAILED task we must keep DB source and
queued source consistent: modify the reactivation branch inside the
findByYoutubeUrl handling so that after detecting existing.status ==
VideoAnalysisTaskStatus.FAILED you update the persisted task's source to the
incoming source as well as its status (use
videoAnalysisTaskPersistencePort.updateSource or the existing persistence method
to set source and updateStatus together), then publish
Events.raise(VideoAnalyzeEvent(...)) using the same updated source (not the old
existing.source), and return existing.copy(status =
VideoAnalysisTaskStatus.PENDING, source = source); ensure you touch
VideoAnalyzeEvent, videoAnalysisTaskPersistencePort.updateStatus/updateSource,
and the existing variable to keep DB and queue sources identical.
In
`@linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt`:
- Line 58: The contains method in InMemoryVideoAnalysisQueueAdapter is violating
ktlint (line >120 and needs a block body newline); change the single-line
expression override fun contains(videoAnalysisTaskId: String): Boolean =
queue.any { it.event.videoAnalysisTaskId == videoAnalysisTaskId } into a
properly formatted block body with a newline and a short line length (for
example use override fun contains(videoAnalysisTaskId: String): Boolean { return
queue.any { it.event.videoAnalysisTaskId == videoAnalysisTaskId } }) so the
expression body is on its own line and the line length stays under 120
characters.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt`:
- Around line 61-96: In executeWithRotation, HttpClient.send(...) can throw
IOException or InterruptedException which currently aborts the rotation; wrap
the send call in try/catch so that IOException is caught, logged (include
proxy.username and exception), and the loop continues to try the next proxy (do
not throw), while InterruptedException should restore the thread interrupt flag
(Thread.currentThread().interrupt()) and rethrow as a LinktripException to
preserve cancel semantics; also ensure requests set an individual timeout (use
HttpRequest.newBuilder(...).timeout(Duration.ofSeconds(15)) when building the
request passed into executeWithRotation) and that each proxy's HttpClient is
created with a connectTimeout
(HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)) in the code that
constructs proxy.httpClient) so slow or dead proxies don't hang forever.
---
Nitpick comments:
In `@linktrip-bootstrap/src/main/resources/application.yml`:
- Around line 47-48: The hardcoded sentinel video id in the configuration
(health-check.sentinel-video-id) should be made configurable via an environment
variable with a default to avoid permanent failures if that video is removed or
region-blocked; change the YAML to read the value from an env var (e.g.
${SENTINEL_VIDEO_ID:TVM6Nswlfbg}) and ensure any code referencing the key (e.g.
isProxyHealthy()) uses the injected value so operators can override
SENTINEL_VIDEO_ID in runtime environments.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt`:
- Around line 96-109: Add a constructor to LinktripException that accepts a
cause (Throwable) and wires it into the super/exception chain, then update
classifyAmbiguousFailure to pass the original TranscriptRetrievalException as
the cause when throwing LinktripException (i.e., replace the current throw
LinktripException(ExceptionCode.BAD_GATEWAY_YOUTUBE, "…") with the new
constructor that includes the cause). Ensure the new LinktripException
constructor preserves message, code (ExceptionCode.BAD_GATEWAY_YOUTUBE) and sets
the cause so exception handlers can access the root
TranscriptRetrievalException.
In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`:
- Line 47: findUnanalyzedVideoIds 구현에서 video_analysis_task와 youtube_video를
anti-join으로 조회할 때 인덱스/쿼리 플랜을 확인하도록 수정하세요:
querydslRepository.findUnanalyzedVideoIds 호출에 사용되는 조인
키(video_analysis_task.youtube_video_id 또는 실제 매칭 컬럼)와 youtube_video의 정렬 컬럼(예:
created_at)에 적절한 인덱스가 존재하는지 DB에 생성하고 EXPLAIN으로 NOT EXISTS/LEFT JOIN이 인덱스를 사용하는지
검증하며, 필요하면 인덱스 추가 또는 limit/스케줄링(주기 단축) 조정 로직을 논의하도록 코멘트를 남기세요.
In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt`:
- Around line 95-105: The query in findUnanalyzedVideoIds performs ORDER BY
video.createdAt ASC with a LEFT JOIN on analysisTask.youtubeUrl, which will
become expensive at scale; ensure the DB has an index on
youtube_video(created_at) to allow index-ordered LIMIT scans and an index on
video_analysis_task(youtube_url) (the unique constraint
uk_video_analysis_task_youtube_url already satisfies this) so the LEFT
JOIN/anti-join can seek efficiently; add or confirm those indexes at the
database level and document/verify with EXPLAIN that the plan uses the indexes
for LIMIT performance.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d17132b5-b70e-4c56-8375-5c107a90fa0f
📒 Files selected for processing (34)
.github/workflows/cicd-release.ymldocker/docker-compose.prod.ymllinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/KeywordAnalyzeService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Source.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisTask.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEvent.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListener.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/youtube/YouTubeCollectService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/input/VideoAnalyzeUseCase.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/YouTubeVideoPersistencePort.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/queue/VideoAnalysisQueuePort.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisQueueConsumerTest.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisTaskTest.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListenerTest.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalyzeServiceTest.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoScheduleServiceTest.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/youtube/YouTubeCollectServiceTest.ktlinktrip-bootstrap/src/main/kotlin/com/linktrip/bootstrap/ApplicationInitializer.ktlinktrip-bootstrap/src/main/resources/application-prod.ymllinktrip-bootstrap/src/main/resources/application.ymllinktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisBackfillScheduler.ktlinktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/VideoDocs.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/VideoAnalyzeAcceptResponse.ktlinktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/YoutubeTranscriptClient.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/properties/YouTubeProperties.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/VideoAnalysisTaskEntity.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt
💤 Files with no reviewable changes (1)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/VideoAnalyzeAcceptResponse.kt
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: In `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`, the concurrent-write safety (duplicate videoId in batch, race condition on uk_youtube_video_video_id) is intentionally deferred. The maintainer (toychip) confirmed the system is currently single-server, so this is not a concern yet. When replication is introduced in the future, ShedLock will be used for distributed locking to address this. At that point, in-batch videoId deduplication should also be applied.
Applied to files:
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/YouTubeVideoPersistencePort.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.ktlinktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisBackfillScheduler.ktlinktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
📚 Learning: 2026-03-18T01:07:53.575Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:07:53.575Z
Learning: Apply the same deferred concurrent-write policy to all Kotlin persistence adapters in the MySQL adapter package: ensure writes are coordinated on a single server with ShedLock considered for the replication phase. For YouTubeVideoPersistenceAdapter.kt (and other adapters in this directory), verify that concurrent writes are serialized or properly guarded, document the policy in code comments, and ensure CI checks or deployment gating will catch any regression where multiple instances might attempt a conflicting write during replication.
Applied to files:
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: Similarly, `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeChannelPersistenceAdapter.kt` likely has the same deferred concurrent-write policy (single server, ShedLock planned for replication phase).
Applied to files:
linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/YouTubeVideoPersistencePort.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/VideoAnalysisTaskEntity.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/YoutubeTranscriptClient.ktlinktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
🪛 GitHub Actions: CI - Pull request
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
[error] 58-58: ktlint failed: Exceeded max line length (120) (cannot be auto-corrected).
[error] 58-58: ktlint failed: Newline expected before expression body.
[error] 58-58: ktlint failed: Missing newline after '{'.
[error] 1-1: Gradle task failed: ':linktrip-output-cache:caffeine:ktlintMainSourceSetCheck' (KtLint found code style violations).
🔇 Additional comments (27)
linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoScheduleServiceTest.kt (1)
40-40: LGTM!
Source.USER를 fixture에 명시적으로 추가하여VideoAnalysisTask의 새 필수 속성과 정합됩니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEvent.kt (1)
6-6: LGTM!이벤트 페이로드에
source를 추가하여 리스너/큐까지 원천 정보를 전달하는 전반적 변경과 일치합니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/KeywordAnalyzeService.kt (1)
67-70: LGTM!배치 플로우에서
Source.BATCH로 명시 호출하여 우선순위 큐 정책과 정합됩니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListener.kt (1)
19-22: LGTM!
event.source를 로그와 enqueue 인자 양쪽에 반영한 점이 적절합니다. 운영 시 소스별 큐 투입 추적이 용이해집니다.linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobConfig.kt (1)
59-59: LGTM!재시도 재투입 시 원래
task.source를 보존하여 USER 요청이 BATCH로 강등되지 않습니다.ApplicationInitializer의 재적재 경로와도 일관됩니다.docker/docker-compose.prod.yml (1)
22-22: LGTM — 배포 전 시크릿 교체 확인 필요env 변수명 변경 자체는 CI/CD,
application-prod.yml과 정합합니다. 배포 전 호스트.env및 GitHub Secrets에서YOUTUBE_PROXY_USERNAME→YOUTUBE_PROXY_USERNAMES(콤마 구분) 교체가 반드시 선행되어야 누락 시 프록시 인증 실패로 수집 파이프라인 전체가 중단됩니다(이미 PR 체크리스트에 명시된 항목).linktrip-bootstrap/src/main/kotlin/com/linktrip/bootstrap/ApplicationInitializer.kt (1)
53-53: LGTM!부팅 시 PENDING 재적재에서
task.source를 그대로 전달하여 재기동 후에도 우선순위가 보존됩니다. DDL 마이그레이션으로 기존 레코드의source컬럼이 non-null로 백필되어야 한다는 점(PR 운영 체크리스트)만 배포 전 확인 필요합니다.linktrip-application/src/main/kotlin/com/linktrip/application/port/input/VideoAnalyzeUseCase.kt (1)
6-11: 시그니처 확장 LGTM
analyzeVideo에source: Source필수 파라미터가 추가되었고, PR 전반의 호출부(USER/BATCH)가 일관되게 업데이트되어 있어 계약 변경이 깔끔합니다.linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisQueueConsumerTest.kt (1)
82-82:VideoAnalyzeEvent에 Source 인자 추가 반영 확인모든
processAnalysis호출이Source.USER로 통일되어 신규 시그니처와 일치합니다. 다만processAnalysis자체가 source에 의존하지 않는다면 현재 구성으로 충분하고, 큐 우선순위 동작 검증은InMemoryVideoAnalysisQueueAdapter테스트 쪽에서 다루고 있을 것으로 보입니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/youtube/YouTubeCollectService.kt (1)
99-102: BATCH source 전달 LGTM수집 파이프라인에서 생성된 분석 요청이
Source.BATCH로 태깅되어 USER 요청보다 뒤에 처리되도록 한 의도가 명확히 드러납니다. 명명 인자 사용도 가독성에 좋습니다.linktrip-application/src/main/kotlin/com/linktrip/application/port/output/queue/VideoAnalysisQueuePort.kt (1)
7-11: enqueue 시그니처 확장 LGTM
source파라미터 추가로 우선순위 큐 어댑터가VideoAnalyzeEvent(id, url, source)를 구성할 수 있게 되었고, 호출부(리스너/초기화/백필 스케줄러)도 일관되게 업데이트되어 있습니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt (1)
46-48: findUnanalyzedVideoIds 위임 구현 LGTM
@Transactional(readOnly = true)도 올바르게 적용되어 있고, 실제 anti-join 로직은 Querydsl 레포지토리에 위임하는 구조라 어댑터 책임이 잘 분리되어 있습니다. 백필 스케줄러에서 10분마다 소량(5건) 소비하는 사용 패턴이라 인덱스만 받쳐주면 운영상 이슈는 없을 것으로 보입니다.linktrip-bootstrap/src/main/resources/application-prod.yml (2)
21-25: 백필 스케줄러 prod 활성화 LGTM주석이 동작(10분/5건, dev 비활성 사유) 의도를 명확히 설명하고 있어 운영 관점에서 이해하기 좋습니다.
batch.video-analysis-backfill.enabled속성명도 스케줄러의@ConditionalOnProperty와 일치하는 것으로 보입니다.
19-19: 환경변수 리네이밍이 완전히 적용됨 - 추가 조치 불필요검증 결과,
YOUTUBE_PROXY_USERNAME→YOUTUBE_PROXY_USERNAMES로의 변경이 완전히 적용되었습니다:
- application-prod.yml, docker-compose.prod.yml, cicd-release.yml 모두 새로운 복수형 이름으로 일관되게 업데이트됨
- 기존 단일형 변수명은 코드베이스 어디에도 남아 있지 않음
- GitHub Actions 배포 워크플로우에서 secrets.YOUTUBE_PROXY_USERNAMES가 올바르게 참조되고 EC2에 전달됨
배포 전 추가 검증이 필요하지 않습니다.
linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Source.kt (1)
1-14: Source enum 도입 LGTM우선순위 간격(0 ↔ 10)을 벌려둬서 추후 중간 티어(예: RETRY, ADMIN 등) 추가 여지를 확보한 점이 좋습니다.
PriorityBlockingQueue에서 동일 priority 처리 시의 FIFO 보장은 별도 시퀀스 타이브레이커로 해결된 것으로 PR 설명에 언급되어 있어 엔트리 비교 로직에 반영되어 있는지만 확인하시면 됩니다.linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/YouTubeVideoPersistencePort.kt (1)
23-28: 포트 메서드 추가 LGTMKDoc에 "오래된 것부터 [limit] 건", "stranded 영상 소진용"으로 계약(정렬·용도)이 명시되어 있어 구현체/호출부 모두 기대치가 분명합니다. MySQL 어댑터 구현과도 시그니처가 일치합니다.
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/VideoAnalysisTaskEntity.kt (1)
36-38: 기존 row 백필 마이그레이션 필수
source컬럼이nullable=false로 추가되었습니다. 배포 전 기존video_analysis_task레코드에 대해 기본값(예:USER또는BATCH)을 채워 넣는 DDL/DML 마이그레이션이 선행되지 않으면 애플리케이션 기동 시 기존 row 조회에서toDomain()이 NPE를 유발하거나, DDL auto 적용 환경에서는 NOT NULL 제약 추가 자체가 실패할 수 있습니다. PR objectives에 운영 절차로 명시되어 있으니 배포 순서를 재확인해 주세요.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/VideoDocs.kt (1)
65-92: 400 응답의 "자막 없는 영상" 예시는 정확하며 유지해야 함검토 결과,
VideoAnalyzeAdapter.kt:65에서 자막 추출 실패(transcript == null)시 여전히BAD_REQUEST_VIDEO예외를 던지고 있으므로, 현재 문서화된 400 응답 예시가 정확합니다. 자막 없는 영상에 대한 예시는 제거할 필요가 없습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.kt (1)
54-68: LGTM!
POST /video/analyze의 응답 계약 전환(상태별 200/202 매핑, COMPLETED 인 경우 schedule 인라인 반환, 그 외엔 빈 리스트로from(...))이 Swagger/GET /video/schedule핸들러와 대칭적으로 잘 정리되었습니다.Source.USER전달도VideoAnalyzeUseCase의 새 시그니처와 일치합니다.linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisBackfillScheduler.kt (1)
29-51: LGTM!10분 주기 cron,
BATCH_SIZE=5의 보수적인 enqueue, 영상별 try/catch 로 한 건 실패가 전체 루프를 죽이지 않도록 한 구성 모두 적절합니다.ConditionalOnProperty로 dev 비활성화 분리도 깔끔합니다. 단일 서버 가정 하 분산 락 미적용은 기존 정책과 일치합니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/properties/YouTubeProperties.kt (1)
9-20: LGTM!
usernames: List<String>로의 전환과isEnabled()의 AND 조건, 그리고HealthCheckProperties.sentinelVideoId기본값 빈 문자열 처리(YoutubeTranscriptClient.isProxyHealthy()에서 false 처리) 모두 일관됩니다.linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalyzeServiceTest.kt (1)
79-103: LGTM!신규 USER/BATCH 분기와, 특히
BATCH 로 만들어진 FAILED → USER 가 재요청시 task.source(원본 audit) 과 재시도 이벤트의 source(현재 trigger) 가 분리되는 의도까지 검증한 테스트 구성이 좋습니다.Also applies to: 171-197
linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisTask.kt (1)
13-24: LGTM!
source필수 필드와create(youtubeUrl, source)시그니처 변경, 그리고 백필 쿼리에서 join key 로 활용되는YOUTUBE_VIDEO_BASE_URL의 가시성 승격 모두 사용처와 일관됩니다.Also applies to: 50-62
linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/YoutubeTranscriptClient.kt (2)
45-59: LGTM!ko 수동 → en 수동 → ko 자동 → en 자동 순 폴백을
runCatching체인으로 깔끔하게 표현했습니다.listTranscripts(videoId)의 네트워크 실패는 chain 진입 전에 그대로 전파되고,fetch()단계 실패는let블록 안에서 자연 전파되므로 프록시/HTTP 실패가 무음 처리되지 않는 점도 확인했습니다.
68-85:listTranscripts()캐싱 동작 확인 결과thoroldvix/youtube-transcript-api 라이브러리는 빌트인 캐싱 메커니즘이 없으며, 동일 영상에 대해서도
listTranscripts()호출 시마다 YouTube에 대해 신규 HTTP 요청을 발생시킵니다. Java 11 HttpClient 또는 사용자 정의 클라이언트를 통해 매번 실제 네트워크/프록시를 거치므로, 현재 구현은 의도된 대로 동작합니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt (1)
152-182: 비용 증가 의도는 잘 명시했으나, 빌드 실패 우려는 타당하지 않습니다.
analyzeByAiVideoIngestion의 미사용 상태를 자세히 설명한 docstring (토큰 소비 25배, 자막 추출 불가 시 와이어업 예정)은 좋습니다. 다만 빌드 실패 가능성에 대한 우려는 프로젝트 설정에 맞지 않습니다:
- detekt 의 style 규칙이 비활성화(
style: active: false)되어 있어 UnusedPrivateMember 검사가 작동하지 않음- ktlint는 코드 포맷터이며 사용되지 않은 코드를 감지하지 않음
- Kotlin 컴파일러도 private 멤버의 미사용에 대해 기본적으로 경고하지 않음
@Suppress("unused")는 필요 없습니다.> Likely an incorrect or invalid review comment.linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt (1)
48-54: LGTM!
queue.take()블로킹 후.event로 언래핑하여 포트 계약(VideoAnalyzeEvent?)을 정확히 충족합니다.InterruptedException발생 시 인터럽트 플래그를 복원하고 null 반환하는 패턴도 적절합니다.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt (1)
48-54:⚠️ Potential issue | 🔴 Criticaldequeue() 계약과 구현의 불일치 명확히 할 필요.
VideoAnalysisQueuePort.dequeue(): VideoAnalyzeEvent?시그니처와 소비자의dequeue() ?: continue패턴은 큐가 비었을 때 null을 반환하는 비블로킹 동작을 암시합니다. 하지만 실제 구현은queue.take()로 무한 대기하며, null은 오직InterruptedException발생 시에만 반환됩니다.현재 소비자가 전용 데몬 스레드에서 실행되므로 블로킹 동작이 효율적이긴 하지만, 인터페이스 계약이 실제 동작을 정확히 반영하지 않습니다. 다음 중 하나를 수행하세요:
- 블로킹 의도를 명시: 인터페이스 문서 또는 JavaDoc에 "이 메서드는 요소가 사용 가능해질 때까지 블로킹한다"고 명시하고, 소비자 코드의
?: continue는 인터럽트 발생 시에만 작동함을 문서화- 타임아웃 기반 폴링으로 변경: 현재 설계가 논블로킹 폴링을 원한다면
poll(timeout, unit)사용 추천🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt` around lines 48 - 54, InMemoryVideoAnalysisQueueAdapter.dequeue currently blocks using queue.take(), but the VideoAnalysisQueuePort.dequeue signature and consumers using "dequeue() ?: continue" imply non‑blocking null-return behavior; either (A) make the blocking intent explicit by updating VideoAnalysisQueuePort.dequeue Javadoc to state "blocks until an element is available (returns null only on interrupt)" and document the consumer's reliance on interrupt→null, or (B) change the adapter to non‑blocking polling by replacing queue.take() with queue.poll(timeout, unit) and return null on timeout while preserving the existing InterruptedException handling (re-interrupt thread and return null); update any related consumer expectations accordingly (refer to InMemoryVideoAnalysisQueueAdapter.dequeue, VideoAnalysisQueuePort.dequeue, and the consumer pattern "dequeue() ?: continue").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt`:
- Around line 81-100: Mask the proxy username before logging anywhere in
ProxyYoutubeClient (e.g., in the logger.warn, logger.debug, logger.info calls
that currently interpolate proxy.username); implement a small helper function
(e.g., maskUsername or maskProxyUser) that redacts most characters and leaves
only a short prefix/suffix or a fixed token, use that helper wherever
proxy.username is logged and avoid putting the raw value into lastNetworkFailure
or any log message—replace direct uses of proxy.username in the logging
statements shown with the masked value.
- Line 75: The loop over proxyClients in ProxyYoutubeClient always starts at
index 0, so fix it by rotating the start index per request: add a per-instance
AtomicInteger (e.g., nextProxyIndex) and on each request getAndIncrement() to
compute a start = abs(nextProxyIndex.getAndIncrement()) % proxyClients.size,
then iterate proxies using an index formula like idx = (start + i) %
proxyClients.size to try each proxy in round-robin order; update the method that
contains the for (proxy in proxyClients) loop to use this wrapped-index
iteration and ensure thread-safety by using the AtomicInteger.
---
Outside diff comments:
In
`@linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt`:
- Around line 48-54: InMemoryVideoAnalysisQueueAdapter.dequeue currently blocks
using queue.take(), but the VideoAnalysisQueuePort.dequeue signature and
consumers using "dequeue() ?: continue" imply non‑blocking null-return behavior;
either (A) make the blocking intent explicit by updating
VideoAnalysisQueuePort.dequeue Javadoc to state "blocks until an element is
available (returns null only on interrupt)" and document the consumer's reliance
on interrupt→null, or (B) change the adapter to non‑blocking polling by
replacing queue.take() with queue.poll(timeout, unit) and return null on timeout
while preserving the existing InterruptedException handling (re-interrupt thread
and return null); update any related consumer expectations accordingly (refer to
InMemoryVideoAnalysisQueueAdapter.dequeue, VideoAnalysisQueuePort.dequeue, and
the consumer pattern "dequeue() ?: continue").
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5afe8f4f-2864-45e6-ad3e-c2e36d9304c4
📒 Files selected for processing (3)
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.ktlinktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/YouTubeVideoCachingAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: 자동 검증 (ktlint + test)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: In `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`, the concurrent-write safety (duplicate videoId in batch, race condition on uk_youtube_video_video_id) is intentionally deferred. The maintainer (toychip) confirmed the system is currently single-server, so this is not a concern yet. When replication is introduced in the future, ShedLock will be used for distributed locking to address this. At that point, in-batch videoId deduplication should also be applied.
Applied to files:
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/YouTubeVideoCachingAdapter.ktlinktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: Similarly, `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeChannelPersistenceAdapter.kt` likely has the same deferred concurrent-write policy (single server, ShedLock planned for replication phase).
Applied to files:
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/YouTubeVideoCachingAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.ktlinktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
🔇 Additional comments (4)
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/YouTubeVideoCachingAdapter.kt (1)
42-43: LGTM — 백필 조회를 캐시하지 않는 처리가 적절합니다.미처리 영상 목록은 스케줄러 실행 시점의 최신 상태가 중요하므로,
@Cacheable없이delegate로 직접 위임한 구현이 의도와 잘 맞습니다.linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt (1)
29-33: 우선순위 큐 구성 LGTM.
compareBy({ source.priority }, { sequence })조합으로 USER 우선, 동일 priority 내 FIFO가AtomicLongtiebreaker로 보장됩니다.PriorityBlockingQueue자체는 unbounded라INITIAL_CAPACITY=16은 초기 힙 크기일 뿐이며 put 시 블로킹되지 않는 점도 의도와 일치합니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt (2)
42-67: 요청 단위 타임아웃 적용 좋습니다.
GET/POST모두REQUEST_TIMEOUT을 설정하고 공통 회전 실행 경로로 보내서 느린 프록시가 컨슈머를 오래 점유하는 위험을 줄였습니다.
76-90: 네트워크 실패/인터럽트/타임아웃 처리가 잘 보강되었습니다.
IOException은 다음 프록시로 넘기고,InterruptedException은 interrupt flag를 복원하며, 연결/요청 타임아웃도 분리되어 있어 이전 장애 전파 위험이 줄었습니다.Also applies to: 123-153
관련 이슈
변경 내용
L1 — 자막 추출 실패 분류 정밀화
YoutubeTranscriptClient단일 게이트웨이로 라이브러리 호출 일원화 (자막 추출 + sentinel ping 같은 빈에서)ProxyYoutubeClient가 HTTP status 코드 분류 후LinktripException(BAD_GATEWAY_YOUTUBE)직접 throwisProxyHealthy()의 sentinel 영상 ping 으로 분류 (sentinel 성공 → 영상 고유 문제 INVALID, sentinel 실패 → 전 프록시 차단 PENDING)analyzeFromVideo→analyzeByAiVideoIngestion리네이밍 + 사유 docstring (토큰 25배)L2 — webshare 프록시 라운드로빈
YouTubeProperties.proxy.usernames: List<String>으로 확장 (단일 username 제거)ProxyYoutubeClient가 username 별HttpClient캐시 보유, 순회하며 시도YOUTUBE_PROXY_USERNAME→YOUTUBE_PROXY_USERNAMES(콤마 구분 리스트)L4 — POST 응답에 결과 인라인 반환
POST /video/analyze반환 타입VideoAnalyzeAcceptResponse→VideoAnalyzeResponse(GET 과 동일 shape)VideoAnalyzeAcceptResponse삭제L5 — USER/BATCH 우선순위 큐 + source 영속화
Sourceenum 신규 (USER priority 0, BATCH priority 10)InMemoryVideoAnalysisQueueAdapter를PriorityBlockingQueue+ sequence tiebreaker 로 교체 → priority 보장 + 동일 source 내 FIFO 보장VideoAnalyzeUseCase.analyzeVideo(url, source)시그니처 확장 (default 제거 → 호출처 명시 강제)VideoController→ USER,YouTubeCollectService/KeywordAnalyzeService→ BATCHVideoAnalysisTask도메인 + 엔티티에source컬럼 추가 (audit 통계 + 재시도 priority 보존)VideoAnalysisRetryJob/ApplicationInitializer가 재 enqueue 시task.source그대로 전달L6 — 미처리 영상 자동 분석 스케줄러
YouTubeVideoPersistencePort.findUnanalyzedVideoIds(limit)신규 — LEFT JOIN + IS NULL anti-join 패턴VideoAnalysisBackfillScheduler신규 — 10분 cron 으로 5건씩 BATCH 우선순위 enqueue@ConditionalOnProperty(batch.video-analysis-backfill.enabled=true)로 환경별 활성화application-prod.yml에 활성 플래그 추가 (dev 자동 실행 차단)체크리스트
Summary by CodeRabbit
새로운 기능
개선사항