Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ public record FurnitureProductInfo(
String furnitureProductName,
String furnitureProductMallName,
Long furnitureProductId,
double similarity
double similarity,
List<String> colors,
List<String> clientColors,
Long listPrice,
Integer discountRate,
Long discountPrice,
String brandName,
Long jjymCount
) {
public static FurnitureProductInfo of(
Long id,
Expand All @@ -35,7 +42,48 @@ public static FurnitureProductInfo of(
furnitureProductName,
furnitureProductMallName,
furnitureProductId,
similarity
similarity,
List.of(),
List.of(),
null,
null,
null,
null,
0L
);
}

public static FurnitureProductInfo of(
Long id,
String furnitureProductImageUrl,
String furnitureProductSiteUrl,
String furnitureProductName,
String furnitureProductMallName,
Long furnitureProductId,
double similarity,
List<String> colors,
List<String> clientColors,
Long listPrice,
Integer discountRate,
Long discountPrice,
String brandName,
Long jjymCount
) {
return new FurnitureProductInfo(
id,
furnitureProductImageUrl,
furnitureProductSiteUrl,
furnitureProductName,
furnitureProductMallName,
furnitureProductId,
similarity,
colors,
clientColors,
listPrice,
discountRate,
discountPrice,
brandName,
jjymCount
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import lombok.extern.slf4j.Slf4j;
import or.sopt.houme.domain.furniture.infrastructure.dto.external.naverShop.FurnitureProductsInfoResponse;
import or.sopt.houme.domain.furniture.infrastructure.dto.external.naverShop.NaverFurnitureProductDto;
import or.sopt.houme.domain.furniture.model.entity.CurationSource;
import or.sopt.houme.domain.furniture.model.entity.FurnitureTag;
import or.sopt.houme.domain.furniture.repository.FurnitureTagRepository;
import or.sopt.houme.domain.furniture.service.CurationFurnitureService;
import or.sopt.houme.domain.furniture.service.CurationRawProductService;
import or.sopt.houme.domain.furniture.service.ImageHashService;
import or.sopt.houme.domain.furniture.service.NaverShopService;
import or.sopt.houme.global.discord.DiscordWebhookService;
Expand All @@ -32,6 +34,7 @@ public class CurationFurnitureScheduler {
private final NaverShopService naverShopService;
private final ImageHashService imageHashService;
private final CurationFurnitureService curationFurnitureService;
private final CurationRawProductService curationRawProductService;
private final DiscordWebhookService discordWebhookService;

@Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")
Expand All @@ -58,15 +61,27 @@ public void refreshCurationResults() {
}

FurnitureTag furnitureTag = furnitureTags.get(i);
List<FurnitureProductsInfoResponse.FurnitureProductInfo> infos = fetchCurationWithRetry(furnitureTag);
if (infos.isEmpty()) {
emptyTags++;
log.warn("큐레이션 배치: 결과 없음 tagId={}", furnitureTag.getId());
boolean hasResult = false;

List<FurnitureProductsInfoResponse.FurnitureProductInfo> naverInfos = fetchNaverCurationWithRetry(furnitureTag);
if (!naverInfos.isEmpty()) {
curationFurnitureService.saveCurationResults(furnitureTag, naverInfos, CurationSource.NAVER);
hasResult = true;
}

List<FurnitureProductsInfoResponse.FurnitureProductInfo> rawInfos = fetchRawCurationWithRetry(furnitureTag);
if (!rawInfos.isEmpty()) {
curationFurnitureService.saveCurationResults(furnitureTag, rawInfos, CurationSource.RAW);
hasResult = true;
}

if (hasResult) {
successTags++;
continue;
}

curationFurnitureService.saveCurationResults(furnitureTag, infos);
successTags++;
emptyTags++;
log.warn("큐레이션 배치: NAVER/RAW 모두 결과 없음 tagId={}", furnitureTag.getId());
}
} catch (Exception e) {
status = "실패";
Expand All @@ -91,24 +106,46 @@ public void refreshCurationResults() {
}
}

private List<FurnitureProductsInfoResponse.FurnitureProductInfo> fetchCurationWithRetry(FurnitureTag furnitureTag) {
private List<FurnitureProductsInfoResponse.FurnitureProductInfo> fetchNaverCurationWithRetry(FurnitureTag furnitureTag) {
for (int attempt = 1; attempt <= MAX_RETRY; attempt++) {
try {
return fetchCurationInfos(furnitureTag);
return fetchNaverCurationInfos(furnitureTag);
} catch (Exception e) {
log.warn("큐레이션 배치 실패: tagId={}, attempt={}", furnitureTag.getId(), attempt, e);
log.warn("네이버 큐레이션 배치 실패: tagId={}, attempt={}", furnitureTag.getId(), attempt, e);
sleep(RETRY_BACKOFF_MILLIS * attempt);
}
}

return List.of();
}

private List<FurnitureProductsInfoResponse.FurnitureProductInfo> fetchCurationInfos(FurnitureTag furnitureTag) {
private List<FurnitureProductsInfoResponse.FurnitureProductInfo> fetchRawCurationWithRetry(FurnitureTag furnitureTag) {
for (int attempt = 1; attempt <= MAX_RETRY; attempt++) {
try {
return fetchRawCurationInfos(furnitureTag);
} catch (Exception e) {
log.warn("RAW 큐레이션 배치 실패: tagId={}, attempt={}", furnitureTag.getId(), attempt, e);
sleep(RETRY_BACKOFF_MILLIS * attempt);
}
}

return List.of();
}

private List<FurnitureProductsInfoResponse.FurnitureProductInfo> fetchNaverCurationInfos(FurnitureTag furnitureTag) {
List<NaverFurnitureProductDto> products = naverShopService.search(furnitureTag.getSearchKeyword(), NAVER_DISPLAY);
return imageHashService.rankByImageSimilarity(furnitureTag.getFurnitureUrl(), products, TOP_N);
}

private List<FurnitureProductsInfoResponse.FurnitureProductInfo> fetchRawCurationInfos(FurnitureTag furnitureTag) {
List<NaverFurnitureProductDto> candidates = curationRawProductService.getCandidatesByFurnitureTag(furnitureTag);
if (candidates.isEmpty()) {
return List.of();
}

return imageHashService.rankByImageSimilarity(furnitureTag.getFurnitureUrl(), candidates, TOP_N);
}

private void sleep(long millis) {
try {
Thread.sleep(millis);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
},
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"}
)
}
Comment on lines 20 to 25
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.

)
Expand All @@ -42,6 +42,10 @@ public class CurationFurniture {
@Column(name = "rank", nullable = false)
private Integer rank;

@Enumerated(EnumType.STRING)
@Column(name = "source", nullable = false)
private CurationSource source;

@Column(name = "similarity", nullable = false)
private Double similarity;

Expand All @@ -52,13 +56,15 @@ public static CurationFurniture of(
FurnitureTag furnitureTag,
RecommendFurniture recommendFurniture,
int rank,
CurationSource source,
double similarity,
LocalDateTime fetchedAt
) {
return CurationFurniture.builder()
.furnitureTag(furnitureTag)
.recommendFurniture(recommendFurniture)
.rank(rank)
.source(source)
.similarity(similarity)
.fetchedAt(fetchedAt)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
Expand All @@ -29,7 +32,8 @@
indexes = {
@Index(name = "idx_raw_source", columnList = "source"),
@Index(name = "idx_raw_category", columnList = "category"),
@Index(name = "idx_raw_fetched_at", columnList = "fetched_at")
@Index(name = "idx_raw_fetched_at", columnList = "fetched_at"),
@Index(name = "idx_raw_furniture_tag_id", columnList = "furniture_tag_id")
},
uniqueConstraints = {
@UniqueConstraint(
Expand Down Expand Up @@ -75,22 +79,39 @@ public class CurationRawProduct {
@Comment("판매 몰 이름")
private String productMallName;

@Column(name = "brand")
@Comment("브랜드")
private String brand;

@Column(name = "list_price")
@Comment("정가")
@Comment("임의 정가")
private Long listPrice;

@Column(name = "discount_rate")
@Comment("할인률(%)")
private Integer discountRate;

@Column(name = "discount_price")
@Comment("할인가")
@Comment("판매가")
private Long discountPrice;

@Column(name = "base_shipping_fee")
@Comment("기본 배송비")
private Long baseShippingFee;

@Column(name = "free_shipping_condition")
@Comment("무료 배송 조건")
private Long freeShippingCondition;

@Column(name = "fetched_at", nullable = false)
@Comment("수집 시각")
private LocalDateTime fetchedAt;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "furniture_tag_id")
@Comment("수동 매핑된 가구 태그")
private FurnitureTag furnitureTag;

public static CurationRawProduct of(
String source,
SoozipCategory category,
Expand Down Expand Up @@ -136,4 +157,8 @@ public void updateFrom(
this.fetchedAt = fetchedAt;
}
}

public void updateFurnitureTag(FurnitureTag furnitureTag) {
this.furnitureTag = furnitureTag;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package or.sopt.houme.domain.furniture.model.entity;

public enum CurationSource {
NAVER,
RAW
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
@Table(
name = "recommend_furnitures",
indexes = {
@Index(name = "idx_furniture_product_id", columnList = "furniture_product_id")
@Index(name = "idx_furniture_product_id", columnList = "furniture_product_id"),
@Index(name = "idx_recommend_source", columnList = "source")
},
uniqueConstraints = {
@UniqueConstraint(
name = "uk_furniture_product_id",
columnNames = {"furniture_product_id"}
name = "uk_source_furniture_product_id",
columnNames = {"source", "furniture_product_id"}
)
}
)
Expand Down Expand Up @@ -47,21 +48,27 @@ public class RecommendFurniture {
@Comment("추천 가구 식별자")
private Long furnitureProductId;

@Enumerated(EnumType.STRING)
@Column(name = "source", nullable = false)
@Comment("큐레이션 소스")
private CurationSource source;


public static RecommendFurniture from(
String furnitureProductImageUrl,
String furnitureProductSiteUrl,
String furnitureProductName,
String furnitureProductMallName,
Long furnitureProductId
Long furnitureProductId,
CurationSource source
){
return RecommendFurniture.builder()
.furnitureProductImageUrl(furnitureProductImageUrl)
.furnitureProductSiteUrl(furnitureProductSiteUrl)
.furnitureProductName(furnitureProductName)
.furnitureProductMallName(furnitureProductMallName)
.furnitureProductId(furnitureProductId)
.source(source)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package or.sopt.houme.domain.furniture.repository;

import or.sopt.houme.domain.furniture.model.entity.CurationFurniture;
import or.sopt.houme.domain.furniture.model.entity.CurationSource;
import or.sopt.houme.domain.furniture.model.entity.FurnitureTag;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -12,7 +13,7 @@
public interface CurationFurnitureRepository extends JpaRepository<CurationFurniture, Long> {

@EntityGraph(attributePaths = "recommendFurniture")
List<CurationFurniture> findAllByFurnitureTagOrderByRankAsc(FurnitureTag furnitureTag);
List<CurationFurniture> findAllByFurnitureTagAndSourceOrderByRankAsc(FurnitureTag furnitureTag, CurationSource source);

void deleteByFurnitureTag(FurnitureTag furnitureTag);
void deleteByFurnitureTagAndSource(FurnitureTag furnitureTag, CurationSource source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package or.sopt.houme.domain.furniture.repository;

import or.sopt.houme.domain.furniture.model.entity.CurationRawProductColor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CurationRawProductColorRepository extends JpaRepository<CurationRawProductColor, Long> {
List<CurationRawProductColor> findAllByCurationRawProductIdIn(List<Long> curationRawProductIds);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package or.sopt.houme.domain.furniture.repository;

import or.sopt.houme.domain.furniture.model.entity.CurationRawProduct;
import or.sopt.houme.domain.furniture.model.entity.FurnitureTag;
import or.sopt.houme.domain.furniture.model.entity.SoozipCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
Expand All @@ -14,4 +15,8 @@ List<CurationRawProduct> findAllBySourceAndCategoryAndProductIdIn(
SoozipCategory category,
List<Long> productIds
);

List<CurationRawProduct> findAllByFurnitureTag(FurnitureTag furnitureTag);

List<CurationRawProduct> findAllByFurnitureTagAndProductIdIn(FurnitureTag furnitureTag, List<Long> productIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import or.sopt.houme.domain.furniture.model.entity.Jjym;

import java.util.List;
import java.util.Map;

public interface JjymRepositoryCustom {

List<Jjym> findAllByUserIdWithFurnitureOrderByCreatedAtDesc(Long userId);

java.util.Optional<Jjym> findByUserIdAndRecommendFurnitureId(Long userId, Long recommendFurnitureId);

Map<Long, Long> countByRecommendFurnitureIds(List<Long> recommendFurnitureIds);
}
Loading
Loading