[Feat] AI 해시태그 추출 및 여행 계획 목록에 박/일, 해시태그 표시#51
Conversation
📝 WalkthroughWalkthroughTripPlanService가 TripPlan 조회 시 각 요약에 대해 days/nights를 계산하고, 관련 videoAnalysisTaskId들로부터 해시태그를 일괄 조회해 각 TripPlanSummary에 붙여 반환하도록 해시태그 도메인/영속성 포트 및 저장/조회 어댑터를 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Service as TripPlanService
participant TripRepo as TripPlanPersistencePort
participant HashtagPort as HashtagPersistencePort
Client->>Service: getTripPlans(memberId, pageRequest)
Service->>TripRepo: findSummariesByMemberId(memberId, pageRequest)
TripRepo-->>Service: Page<TripPlanSummaryRow>(tripPlanRows with days)
Service->>Service: map rows -> TripPlanSummary (compute nights = max(days-1,0))
Service->>HashtagPort: findHashtagNamesByVideoAnalysisTaskIds(list of taskIds)
HashtagPort-->>Service: List<TaskHashtagMapping> (taskId -> hashtagName)
Service->>Service: group mappings by taskId -> Set<String>
Service-->>Client: Page<TripPlanSummary> (with days, nights, hashtags)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~35 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
linktrip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.kt (1)
156-191:⚠️ Potential issue | 🟠 Major
GetTripPlans테스트에서hashtagPortmock stubbing 누락 - NPE 발생 가능
getTripPlans메서드는hashtagPort.findHashtagNamesByVideoAnalysisTaskIds()를 호출하지만, 해당 테스트들에서 이 mock이 stubbing되어 있지 않습니다. Mockito의 기본 동작으로null이 반환되어 line 86-88의groupBy호출 시 NPE가 발생할 수 있습니다.또한,
TripPlanService에서 enrichment하는nights,days,hashtags필드에 대한 assertion이 없어 새로운 기능의 정확성을 검증하지 못합니다.🛡️ 제안된 수정
`@Nested` inner class GetTripPlans { `@Test` fun `결과가 size보다 많으면_hasNext가 true이고 nextCursor가 마지막 항목의 createdAt이다`() { val rows = (1..3).map { createSummaryRow("p$it") } whenever(planPort.findSummariesByMemberId("m1", null, 3)).thenReturn(rows) + whenever(hashtagPort.findHashtagNamesByVideoAnalysisTaskIds(any())).thenReturn(emptyList()) val result = service.getTripPlans("m1", null, 2) assertTrue(result.hasNext) assertEquals(2, result.items.size) assertEquals(result.items.last().tripPlan.createdAt.toString(), result.nextCursor) + // Verify enriched fields + assertEquals(3, result.items[0].days) + assertEquals(2, result.items[0].nights) // maxOf(3 - 1, 0) }다른
GetTripPlans테스트들에도 동일하게hashtagPortstubbing을 추가해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.kt` around lines 156 - 191, The tests in GetTripPlans are missing stubbing for hashtagPort.findHashtagNamesByVideoAnalysisTaskIds which TripPlanService.getTripPlans calls (causing possible NPE); update each test in TripPlanServiceTest:GetTripPlans to stub hashtagPort.findHashtagNamesByVideoAnalysisTaskIds(...) to return an appropriate map or empty map matching the created summary rows, and add assertions verifying the enriched fields nights, days, and hashtags on the returned items to validate the new enrichment logic.
🧹 Nitpick comments (3)
linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Hashtag.kt (1)
13-17:name파라미터 검증 고려
HashtagEntity에서name컬럼의 길이가 50자로 제한되어 있지만, 도메인 레벨에서는 검증이 없습니다. 빈 문자열이나 50자 초과 문자열이 전달될 경우 persistence 레이어에서 예외가 발생할 수 있습니다.💡 도메인 레벨 검증 추가 제안
companion object { fun create(name: String): Hashtag { + require(name.isNotBlank()) { "Hashtag name must not be blank" } + require(name.length <= 50) { "Hashtag name must not exceed 50 characters" } return Hashtag( id = IdGenerator.generate(), name = name, ) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Hashtag.kt` around lines 13 - 17, The Hashtag.create factory currently accepts any string but the persistence layer limits HashtagEntity.name to 50 chars, so add domain-level validation in Hashtag.create (or a private validateName helper used by the Hashtag constructor) to trim input, reject blank/empty names and reject names longer than 50 characters; when invalid, throw a clear runtime exception (e.g., IllegalArgumentException or a DomainValidationException) referencing the failing value so callers fail fast before hitting the persistence layer.linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisResultSaverTest.kt (1)
21-22: 해시태그 저장 로직에 대한 테스트 커버리지 추가를 권장합니다.
HashtagPersistencePort목 주입이 추가되었지만, 해시태그가 실제로 저장되는 시나리오에 대한 테스트가 없습니다. 해시태그 저장 로직의 검증을 위해 별도 테스트 케이스 추가를 고려해 주세요.💡 테스트 케이스 예시
`@Test` fun `해시태그가 포함된 분석 결과 저장 시_해시태그가 조회되고 새 해시태그가 저장된다`() { // given val items = listOf(/* ... */) val hashtags = listOf("서울여행", "맛집탐방") whenever(hashtagPersistencePort.findByNames(hashtags)) .thenReturn(emptyList()) // when saver.save("summary-1", items, hashtags = hashtags) // then verify(hashtagPersistencePort).findByNames(hashtags) verify(hashtagPersistencePort).saveAll(any()) verify(hashtagPersistencePort).saveAllTaskHashtags(any()) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisResultSaverTest.kt` around lines 21 - 22, Add a unit test in VideoAnalysisResultSaverTest that verifies the hashtag persistence interactions: when calling saver.save(...) with a non-empty hashtags list, mock HashtagPersistencePort.findByNames to return an empty list (or partial existing tags) and assert that findByNames was invoked with the provided names and that saveAll and saveAllTaskHashtags on HashtagPersistencePort were called accordingly; reference the HashtagPersistencePort mock, the saver.save(...) invocation, and verify(...) calls to validate the save flow.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/HashtagPersistenceAdapter.kt (1)
23-24: Unique constraint 위반 가능성 - 동시성 고려 필요
saveAll은 단순 JPAsaveAll을 사용하며, 상위 레이어(VideoAnalysisResultSaver.saveHashtags)에서 find-then-create 패턴을 사용합니다. 동일한 해시태그를 포함한 두 영상이 동시에 분석될 경우, 둘 다findByNames에서 빈 결과를 받고 동시에 insert를 시도하여uk_hashtag_name위반이 발생할 수 있습니다.현재 single-server 환경에서는 발생 확률이 낮지만, 향후 확장 시 고려가 필요합니다. Based on learnings, 이 프로젝트는 replication 도입 시 ShedLock을 사용할 계획이므로 현재 단계에서는 허용 가능합니다.
♻️ 향후 개선 제안: INSERT ON DUPLICATE KEY UPDATE 또는 재시도 로직
// Option 1: Native query with ON DUPLICATE KEY `@Query`(""" INSERT INTO hashtag (id, name, created_at, updated_at, deleted) VALUES (:id, :name, NOW(), NOW(), false) ON DUPLICATE KEY UPDATE id = id """, nativeQuery = true) fun insertIgnore(`@Param`("id") id: String, `@Param`("name") name: String) // Option 2: Catch DataIntegrityViolationException and retry override fun saveAll(hashtags: List<Hashtag>): List<Hashtag> { return try { hashtagJpaRepository.saveAll(hashtags.map { HashtagEntity.from(it) }).map { it.toDomain() } } catch (e: DataIntegrityViolationException) { // Re-fetch existing and return findByNames(hashtags.map { it.name }) } }🤖 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/HashtagPersistenceAdapter.kt` around lines 23 - 24, saveAll currently delegates to hashtagJpaRepository.saveAll and can throw a unique-constraint violation (uk_hashtag_name) under concurrent find-then-create flows invoked from VideoAnalysisResultSaver.saveHashtags; fix by making saveAll resilient to that race: wrap the saveAll call in a try/catch for DataIntegrityViolationException (or the appropriate Spring exception) and on catch re-query the persisted hashtags via findByNames(names) and return the merged results, or replace saveAll with a native "insert ignore / ON DUPLICATE KEY UPDATE" insert method on hashtagJpaRepository to perform idempotent inserts; reference: HashtagPersistenceAdapter.saveAll, VideoAnalysisResultSaver.saveHashtags, hashtagJpaRepository, findByNames, uk_hashtag_name.
🤖 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/VideoAnalysisResultSaver.kt`:
- Around line 36-56: In saveHashtags, deduplicate incoming hashtagNames first,
then avoid the non-atomic find-then-create race by attempting to create missing
Hashtag entities (using Hashtag.create and hashtagPersistencePort.saveAll) but
if saveAll fails with a unique constraint/duplicate-key error, catch that
specific exception, re-query existing hashtags via
hashtagPersistencePort.findByNames for the full deduplicated set, and rebuild
the taskHashtags mapping from the union of existing and newly saved hashtags
before calling hashtagPersistencePort.saveAllTaskHashtags; ensure you reference
saveHashtags, hashtagPersistencePort.findByNames, Hashtag.create,
hashtagPersistencePort.saveAll and saveAllTaskHashtags when applying these
changes.
In
`@linktrip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.kt`:
- Around line 7-9: 현재 import 블록에 사용되지 않는 TaskHashtagMapping import가 포함되어 있고
import들이 알파벳(lexicographic) 순으로 정렬되어 있지 않습니다; TripPlanServiceTest의 import 목록에서
TaskHashtagMapping을 제거하고 나머지 import들(예: HashtagPersistencePort,
TravelItineraryItemPersistencePort)을 lexicographic 순서로 정렬되도록 재정렬하세요.
---
Outside diff comments:
In
`@linktrip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.kt`:
- Around line 156-191: The tests in GetTripPlans are missing stubbing for
hashtagPort.findHashtagNamesByVideoAnalysisTaskIds which
TripPlanService.getTripPlans calls (causing possible NPE); update each test in
TripPlanServiceTest:GetTripPlans to stub
hashtagPort.findHashtagNamesByVideoAnalysisTaskIds(...) to return an appropriate
map or empty map matching the created summary rows, and add assertions verifying
the enriched fields nights, days, and hashtags on the returned items to validate
the new enrichment logic.
---
Nitpick comments:
In
`@linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Hashtag.kt`:
- Around line 13-17: The Hashtag.create factory currently accepts any string but
the persistence layer limits HashtagEntity.name to 50 chars, so add domain-level
validation in Hashtag.create (or a private validateName helper used by the
Hashtag constructor) to trim input, reject blank/empty names and reject names
longer than 50 characters; when invalid, throw a clear runtime exception (e.g.,
IllegalArgumentException or a DomainValidationException) referencing the failing
value so callers fail fast before hitting the persistence layer.
In
`@linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisResultSaverTest.kt`:
- Around line 21-22: Add a unit test in VideoAnalysisResultSaverTest that
verifies the hashtag persistence interactions: when calling saver.save(...) with
a non-empty hashtags list, mock HashtagPersistencePort.findByNames to return an
empty list (or partial existing tags) and assert that findByNames was invoked
with the provided names and that saveAll and saveAllTaskHashtags on
HashtagPersistencePort were called accordingly; reference the
HashtagPersistencePort mock, the saver.save(...) invocation, and verify(...)
calls to validate the save flow.
In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/HashtagPersistenceAdapter.kt`:
- Around line 23-24: saveAll currently delegates to hashtagJpaRepository.saveAll
and can throw a unique-constraint violation (uk_hashtag_name) under concurrent
find-then-create flows invoked from VideoAnalysisResultSaver.saveHashtags; fix
by making saveAll resilient to that race: wrap the saveAll call in a try/catch
for DataIntegrityViolationException (or the appropriate Spring exception) and on
catch re-query the persisted hashtags via findByNames(names) and return the
merged results, or replace saveAll with a native "insert ignore / ON DUPLICATE
KEY UPDATE" insert method on hashtagJpaRepository to perform idempotent inserts;
reference: HashtagPersistenceAdapter.saveAll,
VideoAnalysisResultSaver.saveHashtags, hashtagJpaRepository, findByNames,
uk_hashtag_name.
🪄 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: 2bcc6aae-834f-4133-916d-4c0cd64d7eeb
📒 Files selected for processing (25)
linktrip-application/src/main/kotlin/com/linktrip/application/domain/trip/TripPlanService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Hashtag.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisResult.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisResultSaver.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisTaskHashtag.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListener.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/input/TripPlanUseCase.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/HashtagPersistencePort.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/TripPlanPersistencePort.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisResultSaverTest.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListenerTest.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/TripPlanResponse.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/dto/AiApiResponse.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/HashtagPersistenceAdapter.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/TripPlanPersistenceAdapter.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/HashtagEntity.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/VideoAnalysisTaskHashtagEntity.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/HashtagJpaRepository.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/TripPlanQuerydslRepository.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/VideoAnalysisTaskHashtagJpaRepository.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/VideoAnalysisTaskHashtagQuerydslRepository.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/dto/TaskHashtagRow.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/dto/TripPlanSummaryWithVideo.kt
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📚 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/TripPlanPersistenceAdapter.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/HashtagPersistenceAdapter.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-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/VideoAnalysisTaskHashtagJpaRepository.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/dto/TripPlanSummaryWithVideo.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/TripPlanQuerydslRepository.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/VideoAnalysisTaskHashtagQuerydslRepository.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: 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/repository/VideoAnalysisTaskHashtagJpaRepository.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/dto/TripPlanSummaryWithVideo.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/VideoAnalysisTaskHashtagQuerydslRepository.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/HashtagPersistenceAdapter.kt
🪛 GitHub Actions: CI - Pull request
linktrip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.kt
[error] 3-3: ktlint: Imports must be ordered in lexicographic order without any empty lines in-between with "java", "javax", "kotlin" and aliases in the end
[error] 9-9: ktlint: Unused import
🔇 Additional comments (23)
linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisResult.kt (1)
10-10: LGTM!AI 분석 결과에 해시태그 필드가 적절하게 추가되었습니다. AI 요약에 따르면 upstream에서 정규화(trim, empty 제거, 최대 3개 제한)가 처리되므로 이 레벨에서는 추가 검증이 필요하지 않습니다.
linktrip-application/src/main/kotlin/com/linktrip/application/port/input/TripPlanUseCase.kt (1)
55-62: LGTM!
TripPlanSummary에nights,days,hashtags필드가 적절하게 추가되었습니다.hashtags에Set<String>을 사용하여 중복을 방지한 것이 좋습니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisTaskHashtag.kt (1)
6-24: LGTM!
VideoAnalysisTaskHashtag도메인 모델이Hashtag와 일관된 패턴으로 잘 구현되었습니다. N:M 관계를 위한 중간 테이블 도메인 모델로서 적절합니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/VideoAnalysisTaskHashtagEntity.kt (1)
10-37: LGTM!JPA 엔티티가 적절하게 구현되었습니다.
(video_analysis_task_id, hashtag_id)복합 유니크 제약조건이 중복 매핑을 방지합니다. PR 목표에 언급된 대로 UK의 leftmost prefix가video_analysis_task_id단독 쿼리를 커버하므로 별도 인덱스가 불필요합니다.
toDomain()메서드가 없지만, 현재 조회 로직이 QueryDSL을 통해 직접 해시태그 이름을 조회하는 방식이라면 필요하지 않을 수 있습니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/VideoAnalysisTaskHashtagJpaRepository.kt (1)
6-6: LGTM!기본적인 JPA Repository 인터페이스입니다. 학습된 내용에 따르면 동시성 이슈는 현재 단일 서버 환경에서 의도적으로 미뤄진 상태이며, 향후 ShedLock을 통해 처리될 예정입니다.
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/HashtagEntity.kt (1)
10-42: LGTM!
HashtagEntity가 잘 구현되었습니다.toDomain()에서createdAt/updatedAt을 포함하고,from()에서는 JPA 감사(auditing)가 타임스탬프를 처리하므로 생략한 것이 적절합니다.uk_hashtag_name유니크 제약조건으로 해시태그 이름 중복이 방지됩니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/HashtagJpaRepository.kt (1)
6-8: 승인됩니다!
findByNameIn파생 쿼리가 배치 조회를 위해 적절하게 정의되었습니다. 호출부(VideoAnalysisResultSaver.save)에서@Transactional로 감싸져 있으며, 빈 리스트는 이미 라인 40에서if (hashtagNames.isEmpty()) return으로 필터링되고 있어 find → insert 패턴의 원자성이 보장됩니다.linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/HashtagPersistencePort.kt (1)
6-19: LGTM! 깔끔한 포트 인터페이스 설계입니다.헥사고날 아키텍처에 맞게 잘 분리된 포트 인터페이스입니다.
TaskHashtagMappingDTO를 동일 파일에 정의하여 응집도를 높인 점도 좋습니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListener.kt (1)
57-57: LGTM!분석 결과의 해시태그를 저장 로직에 전달하는 변경이 기존 패턴과 일관되게 구현되었습니다.
linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/TripPlanPersistencePort.kt (1)
31-36: LGTM!
TripPlanSummaryRow에days필드 추가가 적절합니다. 여행 일정의 총 일수를 표현하기 위한 확장으로, 쿼리 레이어에서 계산된 값을 전달받는 구조입니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/TripPlanPersistenceAdapter.kt (1)
41-48: LGTM!
days필드 매핑이 기존 패턴을 따르고 있으며, 타입이 일치하여 별도 변환 없이 직접 할당됩니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/dto/TaskHashtagRow.kt (1)
1-6: LGTM!QueryDSL 프로젝션용 DTO로 적절히 정의되었습니다.
HashtagPersistenceAdapter에서TaskHashtagMapping으로 매핑되는 구조와 필드명이 일치합니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/dto/TripPlanSummaryWithVideo.kt (1)
5-10: LGTM!QueryDSL 프로젝션 DTO에
days필드가 적절히 추가되었습니다. 쿼리 레이어에서 계산된 일수를 전달받는 구조입니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/dto/AiApiResponse.kt (2)
16-16: LGTM!AI 응답에서 해시태그 필드를 nullable로 처리하여 하위 호환성을 유지합니다.
42-42: 방어적 파싱 로직이 잘 구현되었습니다.
trim(), 빈 문자열 필터링, 최대 3개 제한, null 시 빈 리스트 반환까지 모든 엣지 케이스가 처리되어 있습니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/TripPlanQuerydslRepository.kt (1)
36-43: LGTM!
maxDaySubQuery의 구현이 올바릅니다.videoAnalysisTaskId로 상관 조인하고 삭제되지 않은 일정 항목만 필터링하여 최대 day 값을 계산합니다. 기본값 1로 fallback하는 것도 적절합니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/trip/TripPlanService.kt (1)
83-101: LGTM! N+1 문제를 방지하는 효율적인 배치 조회 구현해시태그 조회를
videoAnalysisTaskIds로 한 번에 배치 조회하고,groupBy로 Map 변환 후 각 summary에 매핑하는 패턴이 올바릅니다.nights계산 로직maxOf(days - 1, 0)도 1일 여행(0박)을 올바르게 처리합니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt (1)
168-177: LGTM! 해시태그 추출 규칙이 명확하게 정의됨사전 정의된 태그 목록 기반으로 최대 3개까지 선택하도록 규칙이 잘 구성되어 있습니다.
AiApiResponse.toDomain()에서 null 처리 및 trim/filter/take(3) 로직과 일관성 있게 동작합니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/TripPlanResponse.kt (1)
14-16: LGTM!
TripPlanSummary의 새로운 필드들(nights,days,hashtags)이 응답 DTO에 올바르게 매핑되었습니다.linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListenerTest.kt (1)
48-91: LGTM!해시태그 기능 추가에 따른 테스트 업데이트가 완벽합니다.
VideoAnalysisResult에hashtags필드가 추가되었고,save()호출 검증에 해시태그 파라미터가 포함되어 있습니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/VideoAnalysisTaskHashtagQuerydslRepository.kt (1)
17-32: LGTM! 효율적인 배치 조회 구현빈 리스트 guard와
IN절을 사용한 배치 조회가 올바르게 구현되었습니다.Projections.constructor를 사용한 DTO 직접 프로젝션도 적절합니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/HashtagPersistenceAdapter.kt (1)
30-36: LGTM!
findHashtagNamesByVideoAnalysisTaskIds구현이 QueryDSL 리포지토리에 위임하고 결과를TaskHashtagMappingDTO로 변환하는 패턴이 적절합니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisResultSaver.kt (1)
16-34: 해시태그 저장 흐름 연결은 깔끔합니다.
save()에서 일정/비용/상태 업데이트 뒤 해시태그 저장을 같은 트랜잭션으로 묶은 점은 일관성 측면에서 좋습니다.
...pplication/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisResultSaver.kt
Show resolved
Hide resolved
...trip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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-input-http/src/main/kotlin/com/linktrip/input/http/controller/ExceptionAdvice.kt`:
- Line 72: Replace hardcoded response codes in ExceptionAdvice with the
canonical ExceptionCode enum values: change any occurrences like
"BAD_REQUEST_VALIDATION", "NOT_FOUND_RESOURCE", "BAD_GATEWAY_EXTERNAL_API" to
use ExceptionCode.BAD_REQUEST_VALIDATION (or the corresponding enum constants)
when building the response object in the ExceptionAdvice handlers (e.g.,
validation, not-found and external API error handlers). Also add the three enum
constants BAD_REQUEST_VALIDATION, NOT_FOUND_RESOURCE, and
BAD_GATEWAY_EXTERNAL_API to the ExceptionCode enum so the handlers reference a
single source of truth rather than literal strings. Ensure the response uses the
enum constant (or its code accessor) consistently so clients receive the
standardized code.
🪄 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: e3c14a90-302c-40a0-af05-2017eef89e07
📒 Files selected for processing (5)
linktrip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.ktlinktrip-common/src/main/kotlin/com/linktrip/common/exception/LinktripException.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/ExceptionAdvice.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/ExceptionResponse.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/JwtAuthenticationFilter.kt
✅ Files skipped from review due to trivial changes (1)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/ExceptionResponse.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- linktrip-application/src/test/kotlin/com/linktrip/application/domain/trip/TripPlanServiceTest.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 comments (3)
linktrip-common/src/main/kotlin/com/linktrip/common/exception/LinktripException.kt (1)
4-4:exceptionCode보존 추가는 적절합니다.예외 인스턴스에 원본
ExceptionCode를 유지해서 상위 레이어에서 코드 값을 안정적으로 재사용할 수 있습니다.Also applies to: 13-13, 21-21
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/JwtAuthenticationFilter.kt (1)
42-42: 인증 실패 응답 코드 매핑이 일관적입니다.
ExceptionCode.UNAUTHORIZED_AUTHENTICATION_FAILED.name사용으로 응답 스키마 일관성이 좋아졌습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/ExceptionAdvice.kt (1)
38-38:LinktripException경로의 코드 매핑은 올바릅니다.
e.exceptionCode.name사용으로 예외 코드와 응답 코드 간 불일치 위험이 줄었습니다.
관련 이슈
변경 내용
AI 해시태그 자동 추출
여행 계획 목록 응답 확장
nights(박),days(일): MAX(day) 서브쿼리로 계산, nights = days - 1hashtags: 배치 IN절 쿼리 1회로 일괄 조회 후 Map 매핑 (N+1 방지, 총 쿼리 2회)쿼리 최적화
video_analysis_task_hashtag JOIN hashtag WHERE task_id IN (...)배치 1회체크리스트
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선사항