diff --git a/src/main/java/com/tenure/domain/ootd/controller/OotdController.java b/src/main/java/com/tenure/domain/ootd/controller/OotdController.java index ef4c078e..8e12b93f 100644 --- a/src/main/java/com/tenure/domain/ootd/controller/OotdController.java +++ b/src/main/java/com/tenure/domain/ootd/controller/OotdController.java @@ -112,7 +112,7 @@ public BaseResponse getRelatedOotds(@PathVariable Long ootd description = "앱 전용 카메라로 촬영한 착장 사진을 OOTD로 게시합니다. " + "게시 응답은 AI 태그 분석을 기다리지 않고 즉시 반환하며, " + "게시 직후 백그라운드에서 비동기로 사진 전체에 대한 AI 태그 분석이 자동으로 진행됩니다. " - + "분석이 끝나면 tagStatus가 ANALYZING에서 AUTO_UNCONFIRMED로 바뀝니다." + + "자동 태그는 별도 확인 단계 없이 즉시 확정되며, 분석이 끝나면 tagStatus가 ANALYZING에서 CONFIRMED로 바뀝니다." ) @ApiResponse( responseCode = "200", diff --git a/src/main/java/com/tenure/domain/ootd/entity/Ootd.java b/src/main/java/com/tenure/domain/ootd/entity/Ootd.java index 6498a13c..0d907318 100644 --- a/src/main/java/com/tenure/domain/ootd/entity/Ootd.java +++ b/src/main/java/com/tenure/domain/ootd/entity/Ootd.java @@ -106,10 +106,13 @@ public static Ootd createArchived(User owner, String imageUrl, String imageObjec return ootd; } - // AI 자동분석이 끝났을 때 호출한다. 이미 사용자가 확인완료(CONFIRMED)한 뒤라면 되돌리지 않는다. + // AI 자동분석이 끝났을 때 호출한다. auto-tag 플로우는 사용자가 별도로 확인하는 화면이 없으므로 + // 분석이 끝나면 바로 확정(CONFIRMED) 처리해서, OOTD 상세/관련/검색 등 CONFIRMED 태그만 노출하는 + // 화면에서도 자동 태그가 바로 보이게 한다. 이미 ANALYZING을 벗어난 상태라면 되돌리지 않는다. public void markAutoTagsReady() { if (this.tagStatus == OotdTagStatus.ANALYZING) { - this.tagStatus = OotdTagStatus.AUTO_UNCONFIRMED; + this.tagStatus = OotdTagStatus.CONFIRMED; + this.tagConfirmedAt = LocalDateTime.now(); } } diff --git a/src/main/java/com/tenure/domain/tag/service/OotdTagService.java b/src/main/java/com/tenure/domain/tag/service/OotdTagService.java index 2186ee65..6449c377 100644 --- a/src/main/java/com/tenure/domain/tag/service/OotdTagService.java +++ b/src/main/java/com/tenure/domain/tag/service/OotdTagService.java @@ -95,8 +95,17 @@ public void saveAiTags(Long ootdId, List results) { .filter(Objects::nonNull) .toList(); + // auto-tag 플로우는 사용자가 태그를 따로 확인/확정하는 화면이 없으므로, 저장과 동시에 확정(CONFIRMED) + // 처리한다. 그래야 OOTD 상세/관련/검색 등 CONFIRMED 태그만 노출하는 화면과 착용 정보 집계에 바로 반영된다. + tags.forEach(OotdTag::confirm); ootdTagRepository.saveAll(tags); ootd.markAutoTagsReady(); + + tags.stream() + .map(OotdTag::getItem) + .distinct() + .forEach(this::recalculateWearStats); + log.info("AI 태그 저장 완료 - ootdId={}, 저장된 태그 수={}/{} (보유 아이템 매칭 실패분 제외)", ootdId, tags.size(), results.size()); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e18fcce3..ad5517a3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,7 +14,7 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect - format_sql: trueaㅝ + format_sql: true show-sql: false sql: diff --git a/src/test/java/com/tenure/domain/tag/service/OotdTagServiceTest.java b/src/test/java/com/tenure/domain/tag/service/OotdTagServiceTest.java index efc91f63..4691cc52 100644 --- a/src/test/java/com/tenure/domain/tag/service/OotdTagServiceTest.java +++ b/src/test/java/com/tenure/domain/tag/service/OotdTagServiceTest.java @@ -660,6 +660,8 @@ void saveAiTags_filtersOutResultsBelowConfidenceThreshold() { when(ootdRepository.findById(OOTD_ID)).thenReturn(Optional.of(ootd)); when(itemRepository.findByOwner_IdAndItemStatusOrderByCreatedAtDesc(OWNER_ID, ItemStatus.OWNED)) .thenReturn(List.of(jacket, sneakers)); + when(ootdTagRepository.findWearStatsByItemId(anyLong(), eq(TagStatus.CONFIRMED), eq(OotdPublicationStatus.ACTIVE))) + .thenReturn(wearStats(1, LocalDateTime.now())); List results = List.of( aiTagResult("블루종 자켓", "아우터", BigDecimal.valueOf(0.92)), @@ -677,12 +679,12 @@ void saveAiTags_filtersOutResultsBelowConfidenceThreshold() { assertThat(savedTags).hasSize(2); assertThat(savedTags).allSatisfy(tag -> { assertThat(tag.getSource()).isEqualTo(TagSource.AI); - assertThat(tag.getStatus()).isEqualTo(TagStatus.AUTO_UNCONFIRMED); + assertThat(tag.getStatus()).isEqualTo(TagStatus.CONFIRMED); assertThat(tag.getItem()).isNotNull(); }); assertThat(savedTags).extracting(OotdTag::getLabelText) .containsExactlyInAnyOrder("블루종 자켓", "운동화"); - assertThat(ootd.getTagStatus()).isEqualTo(OotdTagStatus.AUTO_UNCONFIRMED); + assertThat(ootd.getTagStatus()).isEqualTo(OotdTagStatus.CONFIRMED); } @Test @@ -694,6 +696,8 @@ void saveAiTags_filtersOutResultsWithOutOfRangeBbox() { when(ootdRepository.findById(OOTD_ID)).thenReturn(Optional.of(ootd)); when(itemRepository.findByOwner_IdAndItemStatusOrderByCreatedAtDesc(OWNER_ID, ItemStatus.OWNED)) .thenReturn(List.of(jacket)); + when(ootdTagRepository.findWearStatsByItemId(anyLong(), eq(TagStatus.CONFIRMED), eq(OotdPublicationStatus.ACTIVE))) + .thenReturn(wearStats(1, LocalDateTime.now())); AiTagResult validResult = aiTagResult("블루종 자켓", "아우터", BigDecimal.valueOf(0.92)); AiTagResult outOfRangeResult = new AiTagResult( @@ -738,7 +742,7 @@ void saveAiTags_skipsResultWhenNoOwnedItemMatches() { verify(ootdTagRepository).saveAll(captor.capture()); assertThat(captor.getValue()).isEmpty(); - assertThat(ootd.getTagStatus()).isEqualTo(OotdTagStatus.AUTO_UNCONFIRMED); + assertThat(ootd.getTagStatus()).isEqualTo(OotdTagStatus.CONFIRMED); } @Test