Skip to content

[CHORE] 큐레이션 로직 구현#417

Open
gdbs1107 wants to merge 16 commits intodevelopfrom
chore/#451/curation
Open

[CHORE] 큐레이션 로직 구현#417
gdbs1107 wants to merge 16 commits intodevelopfrom
chore/#451/curation

Conversation

@gdbs1107
Copy link
Collaborator

@gdbs1107 gdbs1107 commented Feb 6, 2026

📣 Related Issue

📝 Summary

  • curation_raw_product 테이블에 B-1 phase에 추가된 필드가 반영되었습니다
  • soozip 측에서 제공한 프리셋 데이터를 주입하였습니다
  • soozip의 제품이 네이버API 제품과 5/5로 정상적으로 큐레이션 될 수 있도록 로직을 구현하였습니다.

🙏 Question & PR point

겨울 TF팀에서 진행한 회의 결과, 궁극적으로 네이버 API의 의존성을 끊고 모든 데이터를 프리셋으로 만들어두고 거기서 데이터를 제공 할 수 있도록 로직을 수정하는 것으로 결정되었습니다.
수집의 데이터를 큐레이션 과정에서 정상적으로 내려줄 수 있는 로직을 만들게 되었습니다.

이 과정에서 네이버 API의 로직을 최대한 재사용하여 큐레이션을 진행할 수 있도록 구현하였습니다.
로직은 다음과 같습니다.

  • 네이버 API
0. 매일 새벽 두 시, 큐레이션 Batch 로직이 실행됩니다.
1. 네이버 API에서 furniture_tag의 keyword를 이용하여 제품을 50개 제공 받습니다.
2. 해당 50개 중, 가장 유사도가 높은 다섯 개의 제품을 Recommend_furniture 에 저장합니다.
3. 동시에 유사도와 같은 hash 값을 curation_furniture에 저장합니다.
4. 추후 큐레이션 API가 실행되면, furniture_tag를 활용하여 위의 r_f 테이블에서 제품을 그대로 가져옵니다.
  • soozip data
0. 매일 새벽 두 시, 큐레이션 Batch 로직이 실행됩니다.
1. 미리 저장해놓은 soozip의 데이터에서 미리 매핑해놓은 furniture_tag을 이용하여 제품을 n개 제공 받습니다.
2. 해당 n개 중, 가장 유사도가 높은 다섯 개의 제품을 Recommend_furniture 에 저장합니다.
3. 동시에 유사도와 같은 hash 값을 curation_furniture에 저장합니다.
4. 추후 큐레이션 API가 실행되면, furniture_tag를 활용하여 위의 r_f 테이블에서 제품을 그대로 가져옵니다.

사실상 거의 동일한 로직이고 source만 다르다고 이해하시면 될 것 같습니다.



해당 PR까지가 B-1의 마무리 PR이 될 것 같습니다.
이후에는 모든 의사결정을 @PBEM22 @kbt82883 와 함께 회의 후 진행하도록 하겠습니다.

@maehwasoo API 스펙 확인해보시고 컨펌주시길 바랍니다!

  • 수정된 curation API 스펙
  {
    "code": 200,
    "msg": "응답 성공",
    "data": {
      "userName": "string",
      "products": [
        {
          "id": 123,
          "furnitureProductImageUrl": "string",
          "furnitureProductSiteUrl": "string",
          "furnitureProductName": "string",
          "furnitureProductMallName": "string",
          "furnitureProductId": 1234567890,
          "similarity": 0.0,
          "colors": ["string"],
          "clientColors": ["string"],
          "listPrice": 0,
          "discountRate": 0,
          "discountPrice": 0,
          "brandName": "string",
          "jjymCount": 0
        }
      ]
    }
  }

  • 예시값

 {
    "code": 200,
    "msg": "응답 성공",
    "data": {
      "userName": "최융아",
      "products": [
        {
          "id": 398,
          "furnitureProductImageUrl": "https://ecimg.cafe24img.com/pg593b88108200032/janiz/web/product/medium/20260112/47785aff7061d62bcb8f4877d5eb7b6d.jpg",
          "furnitureProductSiteUrl": "https://soozip.co.kr/product/라운드돔-글라스-단스탠드-미드센추리-오브제-탁상조명-어댑터증정/758/category/52/display/1/",
          "furnitureProductName": "라운드돔 글라스 단스탠드 미드센추리 오브제 탁상조명 (어댑터증정)",
          "furnitureProductMallName": "SOOZIP",
          "furnitureProductId": 758,
          "similarity": 0.6063,
          "colors": ["화이트", "브라운"],
          "clientColors": ["화이트", "브라운"],
          "listPrice": 1258696,
          "discountRate": 54,
          "discountPrice": 57900,
          "brandName": "이이공브이샵",
          "jjymCount": 0
        }
      ]
    }
  }

📬 Postman

해당 PR은 프리셋 데이터가 모두 입력된 이후에 머지하도록 하겠습니다.

Summary by CodeRabbit

릴리스 노트

  • New Features
    • 가구 큐레이션 소스 확대: NAVER 및 RAW 소스 지원으로 더욱 다양한 추천 상품 제공
    • 제품 정보 강화: 색상, 가격대, 할인율, 브랜드명 등 상세 정보 추가 표시
    • 큐레이션 시스템 개선: 다중 소스 기반 가구 추천 기능 고도화

@gdbs1107 gdbs1107 self-assigned this Feb 6, 2026
@gdbs1107 gdbs1107 added ✨ feat 기능구현 ✊ 재연 재연 labels Feb 6, 2026
@gdbs1107 gdbs1107 linked an issue Feb 6, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

가구 큐레이션 시스템에 RAW 소스를 추가하여 NAVER와 RAW 두 가지 데이터 소스를 병렬로 처리하도록 확장했습니다. 새로운 CurationSource 구분자를 도입하고, 엔티티와 저장소 계층에 소스 필터링 로직을 추가했으며, 큐레이션 스케줄러와 서비스를 분기 처리 방식으로 리팩토링했습니다.

Changes

Cohort / File(s) Summary
CurationSource 모델 추가
CurationSource.java
NAVER, RAW 두 가지 큐레이션 소스를 나타내는 새로운 Enum 타입 정의
엔티티 모델 확장 (소스 필드)
CurationFurniture.java, RecommendFurniture.java
두 엔티티에 CurationSource 필드 추가 및 고유 제약조건 재설계 (소스 + ID 조합으로 중복 방지)
RAW 가구 데이터 모델
CurationRawProduct.java
brand, baseShippingFee, freeShippingCondition, furnitureTag(관계) 필드 추가 및 updateFurnitureTag 메서드 구현
응답 DTO 필드 확장
FurnitureProductsInfoResponse.java
FurnitureProductInfo 레코드에 colors, clientColors, listPrice, discountRate, discountPrice, brandName, jjymCount 필드 추가 및 팩토리 메서드 오버로딩
저장소 계층 (쿼리 메서드)
CurationFurnitureRepository.java, RecommendFurnitureRepository.java, CurationRawProductRepository.java
소스 필터링이 포함된 새로운 조회/삭제 메서드 추가 (예: findAllByFurnitureTagAndSource, existsBySourceAndFurnitureProductId)
저장소 계층 (신규)
CurationRawProductColorRepository.java, JjymRepositoryCustom.java, JjymRepositoryImpl.java
RAW 제품 색상 조회 및 찜 개수 집계 기능을 위한 새로운 저장소/메서드
서비스 계층 (인터페이스)
CurationFurnitureService.java, RecommendFurnitureService.java
메서드 시그니처에 CurationSource 파라미터 추가 (getCurationProducts, saveCurationResults, saveRecommendFurniture)
서비스 계층 (구현)
CurationFurnitureServiceImpl.java, RecommendFurnitureServiceImpl.java, CurationRawProductService.java
소스별 처리 로직, RAW 메타데이터 빌드(색상, 찜 수), 원본 데이터 필터링 등 구현
스케줄러 리팩토링
CurationFurnitureScheduler.java
NAVER와 RAW 두 개 소스에 대해 별도 재시도/페칭 로직 분기 (fetchNaverCurationWithRetry, fetchRawCurationWithRetry 메서드 추가)
파사드 리팩토링
FurnitureFacadeImpl.java
캐시와 API 호출을 소스별로 분리하여 NAVER 후 RAW 순으로 조회하고 결과 병합

Sequence Diagram

sequenceDiagram
    actor Scheduler as Curation<br/>Scheduler
    participant NaverSvc as NaverShop<br/>Service
    participant ImgHash as ImageHash<br/>Service
    participant RawSvc as CurationRaw<br/>ProductService
    participant SaveSvc as CurationFurniture<br/>Service
    participant DB as Database

    Note over Scheduler,DB: NAVER Source Processing
    Scheduler->>Scheduler: fetchNaverCurationWithRetry()
    Scheduler->>NaverSvc: fetch NAVER products
    NaverSvc-->>Scheduler: product list
    Scheduler->>ImgHash: rank by similarity
    ImgHash-->>Scheduler: ranked products
    Scheduler->>SaveSvc: saveCurationResults(NAVER)
    SaveSvc->>DB: save curations + source=NAVER
    DB-->>SaveSvc: saved results
    SaveSvc-->>Scheduler: completed

    Note over Scheduler,DB: RAW Source Processing
    Scheduler->>Scheduler: fetchRawCurationWithRetry()
    Scheduler->>RawSvc: fetch RAW candidates
    RawSvc->>DB: query curation_raw_products
    DB-->>RawSvc: raw products
    RawSvc-->>Scheduler: candidate list
    Scheduler->>ImgHash: rank by similarity
    ImgHash-->>Scheduler: ranked products
    Scheduler->>SaveSvc: saveCurationResults(RAW)
    SaveSvc->>DB: save curations + source=RAW
    DB-->>SaveSvc: saved results
    SaveSvc-->>Scheduler: completed

    Note over Scheduler,DB: Result Aggregation
    Scheduler->>Scheduler: merge NAVER + RAW results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

refactor:chore

Suggested reviewers

  • PBEM22
  • maehwasoo
  • kbt82883
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.50% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 타이틀은 큐레이션 로직 구현이라는 주요 변경사항을 명확히 요약하고 있어 정확함.
Linked Issues check ✅ Passed PR에서 #415와 #416의 모든 목적(source 필드 추가, RAW 소스 적용, 필드 확장, 배치 로직 수정)을 구현했으며, 복합 유니크 제약조건도 적용됨.
Out of Scope Changes check ✅ Passed 모든 변경사항이 링크된 이슈의 목적(source 기반 캐시 분리, RAW/NAVER 듀얼 소스 처리, 응답 필드 확장)에 직접 관련되어 있음.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/#451/curation

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

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

Caution

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

⚠️ Outside diff range comments (3)
src/main/java/or/sopt/houme/domain/furniture/service/facade/FurnitureFacadeImpl.java (1)

44-45: ⚠️ Potential issue | 🟡 Minor

종료 시간 로그가 시작 시간을 찍고 있어요

Line 44-45에서 formatted를 한 번만 캡처하고, Line 105의 "큐레이션 종료" 로그에서도 같은 값을 사용하고 있어서 실제 종료 시각이 아닌 시작 시각이 찍힙니다. 디버깅이나 모니터링 시 혼란을 줄 수 있어요.

🛠️ 수정 제안
-        log.info("큐레이션 종료:{}",formatted);
+        String endFormatted = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+        log.info("큐레이션 종료:{}", endFormatted);

Also applies to: 105-105

src/main/java/or/sopt/houme/domain/furniture/model/entity/CurationRawProduct.java (1)

115-135: ⚠️ Potential issue | 🟡 Minor

새로 추가된 필드들을 업데이트할 방법이 없어요

brand, baseShippingFee, freeShippingCondition 필드는 엔티티에 추가됐지만, 생성 후 이들을 업데이트할 수 있는 방법이 없네요. 현재 of() 메서드는 외부 source(Naver)에서 제공하는 데이터만 포함하고 있는데, furnitureTag처럼 updateBrand(), updateShippingFees() 같은 update 메서드가 있거나 updateFrom()에 이 필드들을 추가해야 할 것 같습니다. 아니면 이 필드들이 다른 경로로 채워진다면 그 로직을 명확히 해주면 좋을 것 같아요.

src/main/java/or/sopt/houme/domain/furniture/infrastructure/dto/external/naverShop/FurnitureProductsInfoResponse.java (1)

29-54: ⚠️ Potential issue | 🟡 Minor

7-param of()에서 jjymCount0L, 나머지 신규 필드는 null — 기본값 혼재

jjymCount0L로, listPrice/discountRate/discountPrice/brandNamenull로 초기화하고 있는데, 소비 측에서 null 체크 누락 시 NPE 가능성이 있어요. 기본값 정책을 통일하거나, 소비 측에서 null-safe 처리가 되어 있는지 확인이 필요합니다.

🤖 Fix all issues with AI agents
In
`@src/main/java/or/sopt/houme/domain/furniture/model/entity/CurationFurniture.java`:
- Around line 20-25: You changed the unique constraint in entity
CurationFurniture from (furniture_tag_id, rank) to (furniture_tag_id, source,
rank) (constraint name uk_curation_furniture_tag_source_rank); before applying
the migration, run a data-check and cleanup: query existing rows grouped by
furniture_tag_id and rank to find cases with multiple distinct source values,
then either populate missing source values consistently or remove/merge
duplicates so inserting the new constraint will not fail; include this
validation/cleanup as a pre-migration step in your migration script or runbook
and re-run the check after changes to ensure no (furniture_tag_id, rank)
duplicates across different source remain.

In
`@src/main/java/or/sopt/houme/domain/furniture/service/CurationFurnitureServiceImpl.java`:
- Around line 122-123: The repository method deleteByFurnitureTagAndSource
should be annotated with `@Modifying`(clearAutomatically = true) to ensure Spring
Data executes it as a bulk DELETE and automatically synchronizes the persistence
context; update the repository interface where
deleteByFurnitureTagAndSource(...) is declared to add
`@Modifying`(clearAutomatically = true) (and import
org.springframework.data.jpa.repository.Modifying) so subsequent calls in
CurationFurnitureServiceImpl (which calls
curationFurnitureRepository.deleteByFurnitureTagAndSource and saveAll) won't
suffer from stale persistence-context state.

In
`@src/main/java/or/sopt/houme/domain/furniture/service/facade/FurnitureFacadeImpl.java`:
- Around line 54-62: The call to naverShopService.search(keyword, 50) can return
null or an empty list and must be checked before passing to
imageHashService.rankByImageSimilarity; update the code around where products is
assigned (from naverShopService.search) so that if products == null ||
products.isEmpty() you short-circuit (e.g., set rankedInfos to an empty list or
skip the rankByImageSimilarity call and return the empty result) instead of
calling imageHashService.rankByImageSimilarity(furnitureTag.getFurnitureUrl(),
products, CURATION_LIMIT); ensure you follow the same null/empty handling as the
V2 method to avoid NPEs.
🧹 Nitpick comments (13)
src/main/java/or/sopt/houme/domain/furniture/model/entity/RecommendFurniture.java (1)

14-17: source 단독 인덱스는 실효성이 낮을 수 있어요.

source 컬럼은 현재 NAVER/RAW 두 값뿐이라 카디널리티가 매우 낮습니다. 이런 경우 DB 옵티마이저가 인덱스를 타지 않고 풀스캔하는 게 더 효율적이라 판단할 가능성이 높아요. 게다가 uk_source_furniture_product_id 유니크 제약이 이미 (source, furniture_product_id) 복합 인덱스를 내부적으로 생성하므로, source 선행 컬럼 기반 조회는 이미 커버됩니다.

불필요한 인덱스는 쓰기 성능에 오버헤드를 주니, 실제 쿼리 패턴에서 source 단독 필터가 빈번한지 확인해보고 제거를 고려해보세요.

🔧 인덱스 제거 제안
         indexes = {
                 `@Index`(name = "idx_furniture_product_id", columnList = "furniture_product_id"),
-                `@Index`(name = "idx_recommend_source", columnList = "source")
         },
src/main/java/or/sopt/houme/domain/furniture/service/CurationRawProductService.java (2)

28-58: NaverFurnitureProductDto를 RAW 소스에도 재사용하는 부분 — 나중에 리네이밍 고려해보면 좋겠어요.

로직 자체는 정확합니다. null 가드, 필터링, 중복 제거(first wins) 모두 잘 되어 있어요.

다만 NaverFurnitureProductDto라는 이름이 네이버 전용처럼 보이는데 RAW 소스에서도 동일하게 사용하고 있어서, 나중에 FurnitureProductDto 같은 소스-중립적인 이름으로 리네이밍하면 코드 읽을 때 혼란을 줄일 수 있을 것 같습니다. 지금 당장은 기능에 문제없으니 급하진 않고요.


39-57: Collectors.toMap의 결과 순서가 비결정적이에요 — 현재 사용 맥락에서는 괜찮아 보이지만 참고용으로.

Collectors.toMap은 내부적으로 HashMap을 사용해서 .values().stream().toList() 결과의 순서가 보장되지 않습니다. 이 메서드의 결과가 이후 유사도 기반으로 정렬/랭킹된다면 문제없지만, 만약 이 단계에서 특정 순서(예: DB 조회 순서)를 유지해야 하는 요구사항이 생기면 LinkedHashMap을 사용하는 toMap 오버로드로 변경이 필요합니다.

src/main/java/or/sopt/houme/domain/furniture/service/facade/FurnitureFacadeImpl.java (2)

106-109: Stream fully-qualified 사용 & 응답 합치기 로직

java.util.stream.Stream.concat(...) 대신 import를 추가해서 쓰면 더 깔끔합니다. 사소하지만 가독성 차이가 있어요.

♻️ 리팩터 제안

파일 상단 import 추가:

import java.util.stream.Stream;
         return FurnitureProductsInfoResponse.of(
                 user.getName(),
-                java.util.stream.Stream.concat(naverInfos.stream(), rawInfos.stream()).toList()
+                Stream.concat(naverInfos.stream(), rawInfos.stream()).toList()
         );

126-128: CURATION_LIMIT 상수가 ForPlan 메서드에서는 미적용

Line 31에 CURATION_LIMIT = 5를 잘 정의해두셨는데, Line 128과 170에서는 여전히 하드코딩 5를 쓰고 있어요. 나중에 값 변경 시 누락될 수 있으니 통일해두면 좋겠습니다.

Also applies to: 168-170

src/main/java/or/sopt/houme/domain/furniture/repository/RecommendFurnitureRepository.java (1)

11-11: java.util.Optional fully-qualified 사용

import로 빼면 더 깔끔해요. 사소한 부분이지만요.

♻️ 리팩터 제안

파일 상단에 import 추가:

import java.util.Optional;
-    java.util.Optional<RecommendFurniture> findBySourceAndFurnitureProductId(CurationSource source, Long furnitureProductId);
+    Optional<RecommendFurniture> findBySourceAndFurnitureProductId(CurationSource source, Long furnitureProductId);
src/main/java/or/sopt/houme/domain/furniture/service/RecommendFurnitureServiceImpl.java (2)

48-51: existsBy + findBy 이중 쿼리 → findBy 하나로 충분해요

existsBySourceAndFurnitureProductId로 조회한 뒤 바로 findBySourceAndFurnitureProductId를 또 호출하고 있어서, 같은 조건으로 DB 쿼리가 2번 나가요. findBy만 호출해서 Optional로 분기하면 쿼리 1회로 줄일 수 있고, exists 체크와 find 사이에 데이터가 변경되는 TOCTOU 문제도 자연스럽게 사라집니다.

♻️ 개선 제안
-            boolean exists = recommendFurnitureRepository.existsBySourceAndFurnitureProductId(source, productId);
-            if (exists) {
-                entity = recommendFurnitureRepository.findBySourceAndFurnitureProductId(source, productId)
-                        .orElseThrow();
-            } else {
+            var existing = recommendFurnitureRepository.findBySourceAndFurnitureProductId(source, productId);
+            if (existing.isPresent()) {
+                entity = existing.get();
+            } else {
                 entity = RecommendFurniture.from(
                         furnitureProductInfo.furnitureProductImageUrl(),
                         furnitureProductInfo.furnitureProductSiteUrl(),
                         furnitureProductInfo.furnitureProductName(),
                         furnitureProductInfo.furnitureProductMallName(),
                         productId,
                         source
                 );
                 entity = recommendFurnitureRepository.save(entity);
             }

23-29: saveRecommendFurnituresaveSingleRecommendFurniture를 그대로 위임만 하고 있어요

public 메서드가 private 메서드를 단순 호출만 하는 구조라 불필요한 indirection입니다. 현재 로직이 확장될 예정이 아니라면 하나로 합치는 게 깔끔해요.

src/main/java/or/sopt/houme/domain/furniture/infrastructure/dto/external/naverShop/FurnitureProductsInfoResponse.java (1)

13-27: naverShop 패키지에 있지만 RAW 소스에서도 사용되는 공용 DTO에요

FurnitureProductInfo가 이제 NAVER뿐 아니라 RAW 소스의 큐레이션 결과도 담고 있어서, naverShop 패키지에 위치하는 게 의미적으로 안 맞아요. 필드도 14개까지 늘어났고요. 지금 당장은 아니더라도, 향후 공통 DTO 패키지로 분리하거나 소스별 DTO를 나누는 걸 고려해보면 좋겠습니다.

src/main/java/or/sopt/houme/domain/furniture/service/CurationFurnitureServiceImpl.java (2)

56-80: rawMeta != null 반복 체크 — 빈 기본 객체로 간소화 가능

Lines 59~76에서 rawMeta != null ? rawMeta.xxx() : ... 패턴이 6번 반복돼요. 빈 기본값을 가진 RawProductMeta를 하나 만들어두면 null 분기 없이 깔끔해집니다.

♻️ 개선 제안

RawProductMeta에 기본 빈 인스턴스를 추가:

private static final RawProductMeta EMPTY = new RawProductMeta(
        List.of(), List.of(), null, null, null, null
);

그 후 매핑 시:

-                    RawProductMeta rawMeta = rawMetaByProductId.get(recommendFurniture.getFurnitureProductId());
-                    List<String> colors = rawMeta != null ? rawMeta.colors() : List.of();
-                    List<String> clientColors = rawMeta != null ? rawMeta.clientColors() : List.of();
+                    RawProductMeta rawMeta = rawMetaByProductId.getOrDefault(
+                            recommendFurniture.getFurnitureProductId(), RawProductMeta.EMPTY);

이후 rawMeta.colors(), rawMeta.listPrice() 등 직접 접근 가능.


136-216: buildRawMetaByProductId 메서드가 80줄 — 가독성 면에서 분리 고려

color 집계 로직(Lines 177-197)과 최종 meta 조립 로직(Lines 199-214)을 별도 private 메서드로 분리하면 각 단계의 의도가 명확해져요. 지금 당장 급하진 않지만, 이 메서드가 더 커지기 전에 나누는 게 좋겠습니다.

src/main/java/or/sopt/houme/domain/furniture/infrastructure/scheduler/CurationFurnitureScheduler.java (2)

109-133: fetchNaverCurationWithRetryfetchRawCurationWithRetry가 거의 동일한 코드에요

로그 메시지와 내부 호출 메서드만 다르고 retry 구조는 완전히 같아서, 공통 retry 헬퍼로 추출하면 중복을 줄일 수 있어요.

♻️ 공통 retry 헬퍼 제안
+    private List<FurnitureProductsInfoResponse.FurnitureProductInfo> fetchWithRetry(
+            FurnitureTag furnitureTag,
+            java.util.function.Function<FurnitureTag, List<FurnitureProductsInfoResponse.FurnitureProductInfo>> fetcher,
+            String sourceName
+    ) {
+        for (int attempt = 1; attempt <= MAX_RETRY; attempt++) {
+            try {
+                return fetcher.apply(furnitureTag);
+            } catch (Exception e) {
+                log.warn("{} 큐레이션 배치 실패: tagId={}, attempt={}", sourceName, furnitureTag.getId(), attempt, e);
+                sleep(RETRY_BACKOFF_MILLIS * attempt);
+            }
+        }
+        return List.of();
+    }

호출부:

List<...> naverInfos = fetchWithRetry(furnitureTag, this::fetchNaverCurationInfos, "네이버");
List<...> rawInfos = fetchWithRetry(furnitureTag, this::fetchRawCurationInfos, "RAW");

29-31: MAX_RETRY = 2는 최대 시도 횟수가 2회라는 의미 — 네이밍 주의

보통 MAX_RETRY라고 하면 "재시도 횟수"로 읽히는데, 여기서는 attempt <= MAX_RETRY이므로 사실상 총 시도 2회(= 재시도 1회)예요. 의도된 동작이라면 OK인데, 혼동 여지가 있으니 MAX_ATTEMPTS로 바꾸거나 값을 3으로 조정하는 것도 고려해보세요.

Comment on lines 20 to 25
uniqueConstraints = {
@UniqueConstraint(
name = "uk_curation_furniture_tag_rank",
columnNames = {"furniture_tag_id", "rank"}
name = "uk_curation_furniture_tag_source_rank",
columnNames = {"furniture_tag_id", "source", "rank"}
)
}
Copy link

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:

# 마이그레이션 파일 디렉토리 탐색
find . -type d \( -name "db" -o -name "migration" -o -name "migrations" -o -name "flyway" -o -name "liquibase" \) 2>/dev/null | head -20

Repository: TEAM-HOUME/HOUME-SERVER

Length of output: 49


🏁 Script executed:

# SQL/마이그레이션 파일 찾기
git ls-files | grep -E '\.(sql|xml|yaml)$' | grep -iE '(db|migration|flyway|liquibase)' | head -30

Repository: TEAM-HOUME/HOUME-SERVER

Length of output: 49


🏁 Script executed:

# 기존 constraint 이름 검색
rg "uk_curation_furniture_tag" --type=sql --type=yaml --type=xml 2>/dev/null || echo "No matches found"

Repository: TEAM-HOUME/HOUME-SERVER

Length of output: 83


🏁 Script executed:

# Hibernate DDL 설정 확인
fd -e properties -e yml | xargs rg "hibernate.ddl|spring.jpa.hibernate" 2>/dev/null

Repository: TEAM-HOUME/HOUME-SERVER

Length of output: 51


🏁 Script executed:

# CurationFurniture.java 전체 내용 확인
cat src/main/java/or/sopt/houme/domain/furniture/model/entity/CurationFurniture.java

Repository: TEAM-HOUME/HOUME-SERVER

Length of output: 2258


유니크 제약 변경 - 데이터 정합성 확인 필요

제약 조건이 (furniture_tag_id, rank)(furniture_tag_id, source, rank)로 변경된 건 로직상 정확해요. NAVER/RAW 각각 동일 태그에서 rank 1~5를 가질 수 있어야 하니까요.

다만 한 가지 확인 필요한데, 만약 현재 운영 DB에 기존 제약 조건으로 인해 (furniture_tag_id, rank) 조합이 유일한 데이터들이 있다면, 새 제약이 적용될 때 데이터 정합성 이슈가 발생할 수 있습니다. 예를 들어 같은 tag + rank지만 서로 다른 source의 데이터가 남아있으면 제약 위반이 됩니다.

마이그레이션 전에 기존 데이터를 확인하고, 필요하면 source 값을 먼저 채우거나 중복 데이터를 정리하는 절차가 필요할 것 같습니다.

🤖 Prompt for AI Agents
In
`@src/main/java/or/sopt/houme/domain/furniture/model/entity/CurationFurniture.java`
around lines 20 - 25, You changed the unique constraint in entity
CurationFurniture from (furniture_tag_id, rank) to (furniture_tag_id, source,
rank) (constraint name uk_curation_furniture_tag_source_rank); before applying
the migration, run a data-check and cleanup: query existing rows grouped by
furniture_tag_id and rank to find cases with multiple distinct source values,
then either populate missing source values consistently or remove/merge
duplicates so inserting the new constraint will not fail; include this
validation/cleanup as a pre-migration step in your migration script or runbook
and re-run the check after changes to ensure no (furniture_tag_id, rank)
duplicates across different source remain.

Comment on lines +122 to 123
curationFurnitureRepository.deleteByFurnitureTagAndSource(furnitureTag, source);
curationFurnitureRepository.saveAll(curations);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# CurationFurnitureRepository 파일 찾기
find . -name "*CurationFurnitureRepository*" -type f

Repository: TEAM-HOUME/HOUME-SERVER

Length of output: 157


🏁 Script executed:

#!/bin/bash
# deleteByFurnitureTagAndSource 메서드와 `@Modifying` 설정 확인
rg -n -B2 -A5 'deleteByFurnitureTagAndSource' --type=java

Repository: TEAM-HOUME/HOUME-SERVER

Length of output: 1604


🏁 Script executed:

cat -n src/main/java/or/sopt/houme/domain/furniture/repository/CurationFurnitureRepository.java

Repository: TEAM-HOUME/HOUME-SERVER

Length of output: 1030


deleteByFurnitureTagAndSource 메서드에 @Modifying 애노테이션 추가 필요

확인 결과 리포지토리의 deleteByFurnitureTagAndSource 메서드에 @Modifying 애노테이션이 없네요. 이 derived query 메서드는 Spring Data가 bulk DELETE 쿼리로 구현하는데, @Modifying(clearAutomatically = true)를 추가해야 persistence context와의 동기화 문제를 방지할 수 있어요. 현재 상태면 delete 이후 다른 영속성 컨텍스트 작업에서 예상치 못한 동작이 발생할 수 있습니다.

🤖 Prompt for AI Agents
In
`@src/main/java/or/sopt/houme/domain/furniture/service/CurationFurnitureServiceImpl.java`
around lines 122 - 123, The repository method deleteByFurnitureTagAndSource
should be annotated with `@Modifying`(clearAutomatically = true) to ensure Spring
Data executes it as a bulk DELETE and automatically synchronizes the persistence
context; update the repository interface where
deleteByFurnitureTagAndSource(...) is declared to add
`@Modifying`(clearAutomatically = true) (and import
org.springframework.data.jpa.repository.Modifying) so subsequent calls in
CurationFurnitureServiceImpl (which calls
curationFurnitureRepository.deleteByFurnitureTagAndSource and saveAll) won't
suffer from stale persistence-context state.

Comment on lines +54 to +62
log.info("네이버 API 호출을 시작합니다");
String keyword = furnitureTag.getSearchKeyword();
List<NaverFurnitureProductDto> products = naverShopService.search(keyword, 50);

// 2. FastAPI 호출 → 유사도 기반 상위 상품 리스트만 반환
// 12/05 FAST API 삭제
log.info("유사도 기반 네이버 상품 조회를 시작합니다");
List<FurnitureProductsInfoResponse.FurnitureProductInfo> rankedInfos =
imageHashService.rankByImageSimilarity(furnitureTag.getFurnitureUrl(), products, CURATION_LIMIT);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

네이버 API 응답 null/empty 체크 누락

naverShopService.search(keyword, 50) 반환값이 null이거나 빈 리스트일 때, Line 62의 rankByImageSimilarity로 바로 넘기면 NPE가 발생할 수 있어요. V2 메서드(Line 159)에서는 products == null || products.isEmpty() 체크를 하고 있는데, 여기는 빠져 있네요.

🐛 수정 제안
             log.info("네이버 API 호출을 시작합니다");
             String keyword = furnitureTag.getSearchKeyword();
             List<NaverFurnitureProductDto> products = naverShopService.search(keyword, 50);
+
+            if (products == null || products.isEmpty()) {
+                naverInfos = List.of();
+            } else {
             // 2. FastAPI 호출 → 유사도 기반 상위 상품 리스트만 반환
             // 12/05 FAST API 삭제
             log.info("유사도 기반 네이버 상품 조회를 시작합니다");
             List<FurnitureProductsInfoResponse.FurnitureProductInfo> rankedInfos =
                     imageHashService.rankByImageSimilarity(furnitureTag.getFurnitureUrl(), products, CURATION_LIMIT);

             // 2-1. 최종반환된 리스트를 기반으로 추천가구 엔티티 저장하고, 큐레이션 결과 저장
             log.info("네이버 큐레이션 결과 저장");
             naverInfos = curationFurnitureService.saveCurationResults(
                     furnitureTag,
                     rankedInfos,
                     CurationSource.NAVER
             );
+            }
🤖 Prompt for AI Agents
In
`@src/main/java/or/sopt/houme/domain/furniture/service/facade/FurnitureFacadeImpl.java`
around lines 54 - 62, The call to naverShopService.search(keyword, 50) can
return null or an empty list and must be checked before passing to
imageHashService.rankByImageSimilarity; update the code around where products is
assigned (from naverShopService.search) so that if products == null ||
products.isEmpty() you short-circuit (e.g., set rankedInfos to an empty list or
skip the rankByImageSimilarity call and return the empty result) instead of
calling imageHashService.rankByImageSimilarity(furnitureTag.getFurnitureUrl(),
products, CURATION_LIMIT); ensure you follow the same null/empty handling as the
V2 method to avoid NPEs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✊ 재연 재연 ✨ feat 기능구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] 큐레이션 B-1 api 스펙수정 [CHORE] 큐레이션 가구 필드 세부 조정

1 participant