-
Notifications
You must be signed in to change notification settings - Fork 1
Fix/357 brand sponsor list #360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
deaa812
07f2f65
c212989
425f027
77e42ff
1419bcd
d30b559
7fe4e0c
4a9dad8
c83e4a0
7785aa3
ede3b9d
4ca2047
63d111c
a227c32
308cfbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,8 @@ | |
| import com.example.RealMatch.brand.domain.entity.BrandDescribeTag; | ||
| import com.example.RealMatch.brand.domain.entity.BrandImage; | ||
| import com.example.RealMatch.brand.domain.entity.BrandLike; | ||
| import com.example.RealMatch.brand.domain.entity.BrandSponsorImage; | ||
| import com.example.RealMatch.brand.domain.entity.BrandSponsorInfo; | ||
| import com.example.RealMatch.brand.domain.entity.enums.IndustryType; | ||
| import com.example.RealMatch.brand.domain.repository.BrandAvailableSponsorRepository; | ||
| import com.example.RealMatch.brand.domain.repository.BrandCategoryRepository; | ||
|
|
@@ -26,6 +28,7 @@ | |
| import com.example.RealMatch.brand.domain.repository.BrandImageRepository; | ||
| import com.example.RealMatch.brand.domain.repository.BrandLikeRepository; | ||
| import com.example.RealMatch.brand.domain.repository.BrandRepository; | ||
| import com.example.RealMatch.brand.domain.repository.BrandSponsorInfoRepository; | ||
| import com.example.RealMatch.brand.exception.BrandErrorCode; | ||
| import com.example.RealMatch.brand.presentation.dto.request.BrandBeautyCreateRequestDto; | ||
| import com.example.RealMatch.brand.presentation.dto.request.BrandBeautyUpdateRequestDto; | ||
|
|
@@ -56,19 +59,17 @@ | |
| import com.example.RealMatch.user.domain.repository.UserRepository; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| @Slf4j | ||
| public class BrandService { | ||
|
|
||
| private final BrandRepository brandRepository; | ||
| private final BrandLikeRepository brandLikeRepository; | ||
| private final BrandCategoryViewRepository brandCategoryViewRepository; | ||
| private final BrandCategoryRepository brandCategoryRepository; | ||
| private final BrandAvailableSponsorRepository brandAvailableSponsorRepository; | ||
| private final BrandSponsorInfoRepository brandSponsorInfoRepository; | ||
| private final BrandDescribeTagRepository brandDescribeTagRepository; | ||
| private final BrandImageRepository brandImageRepository; | ||
|
|
||
|
|
@@ -78,7 +79,6 @@ public class BrandService { | |
| private final TagRepository tagRepository; | ||
|
|
||
| private final UserRepository userRepository; | ||
|
|
||
| private static final Pattern URL_PATTERN = Pattern.compile("^https?://([\\da-z.-]+)\\.([a-z.]{2,6})[/\\w .-]*/?$"); | ||
|
|
||
| // ******** // | ||
|
|
@@ -222,52 +222,115 @@ public SponsorProductDetailResponseDto getSponsorProductDetail(Long brandId, Lon | |
| throw new IllegalArgumentException("해당 브랜드의 제품이 아닙니다."); | ||
| } | ||
|
|
||
| List<String> mockImageUrls = List.of( | ||
| "https://cdn.example.com/products/100/1.png", | ||
| "https://cdn.example.com/products/100/2.png", | ||
| "https://cdn.example.com/products/100/3.png" | ||
| ); | ||
| BrandSponsorInfo sponsorInfo = brandSponsorInfoRepository.findBySponsorIdWithItems(product.getId()) | ||
| .orElse(null); | ||
| return buildSponsorProductDetailResponse(brand, product, sponsorInfo); | ||
| } | ||
|
|
||
| List<String> mockCategories = List.of("스킨케어", "메이크업"); | ||
| @Transactional(readOnly = true) | ||
| public List<SponsorProductListResponseDto> getSponsorProducts(Long brandId) { | ||
| Brand brand = brandRepository.findById(brandId) | ||
| .orElseThrow(() -> new ResourceNotFoundException("브랜드 정보를 찾을 수 없습니다.")); | ||
|
|
||
| List<SponsorItemDto> mockItems = List.of( | ||
| SponsorItemDto.builder().itemId(1L).availableType("SAMPLE").availableQuantity(1).availableSize(50).sizeUnit("ml").build(), | ||
| SponsorItemDto.builder().itemId(2L).availableType("FULL").availableQuantity(1).availableSize(100).sizeUnit("ml").build() | ||
| ); | ||
| List<BrandAvailableSponsor> products = brandAvailableSponsorRepository.findByBrandIdWithImages(brandId); | ||
| List<Long> sponsorIds = products.stream() | ||
| .map(BrandAvailableSponsor::getId) | ||
| .collect(Collectors.toList()); | ||
| Map<Long, BrandSponsorInfo> sponsorInfoBySponsorId = sponsorIds.isEmpty() | ||
| ? Map.of() | ||
| : brandSponsorInfoRepository.findBySponsorIdInWithItems(sponsorIds) | ||
| .stream() | ||
| .collect(Collectors.toMap(info -> info.getSponsor().getId(), Function.identity())); | ||
|
|
||
| SponsorInfoDto sponsorInfo = SponsorInfoDto.builder() | ||
| .items(mockItems) | ||
| .shippingType("CREATOR_PAY") | ||
| .build(); | ||
| return products.stream() | ||
| .map(product -> buildSponsorProductListResponse(brand, product, sponsorInfoBySponsorId.get(product.getId()))) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| ActionDto action = ActionDto.builder() | ||
| .canProposeCampaign(true) | ||
| .proposeCampaignCtaText("캠페인 제안하기") | ||
| .build(); | ||
| private SponsorProductDetailResponseDto buildSponsorProductDetailResponse( | ||
| Brand brand, | ||
| BrandAvailableSponsor product, | ||
| BrandSponsorInfo sponsorInfo | ||
| ) { | ||
| List<String> imageUrls = buildProductImageUrls(product); | ||
| List<String> categories = buildCategories(brand); | ||
| SponsorInfoDto sponsorInfoDto = buildSponsorInfo(brand.getIndustryType(), sponsorInfo); | ||
| ActionDto action = buildAction(); | ||
|
|
||
| return SponsorProductDetailResponseDto.builder() | ||
| .brandId(brand.getId()) | ||
| .brandName(brand.getBrandName()) | ||
| .productId(product.getId()) | ||
| .productName(product.getName()) | ||
| .productDescription(product.getCampaign().getDescription()) | ||
| .productImageUrls(mockImageUrls) | ||
| .categories(mockCategories) | ||
| .sponsorInfo(sponsorInfo) | ||
| .productImageUrls(imageUrls) | ||
| .categories(categories) | ||
| .sponsorInfo(sponsorInfoDto) | ||
| .action(action) | ||
| .build(); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public List<SponsorProductListResponseDto> getSponsorProducts(Long brandId) { | ||
| brandRepository.findById(brandId) | ||
| .orElseThrow(() -> new ResourceNotFoundException("브랜드 정보를 찾을 수 없습니다.")); | ||
| private SponsorProductListResponseDto buildSponsorProductListResponse( | ||
| Brand brand, | ||
| BrandAvailableSponsor product, | ||
| BrandSponsorInfo sponsorInfo | ||
| ) { | ||
| List<String> imageUrls = buildProductImageUrls(product); | ||
| List<String> categories = buildCategories(brand); | ||
| SponsorInfoDto sponsorInfoDto = buildSponsorInfo(brand.getIndustryType(), sponsorInfo); | ||
| ActionDto action = buildAction(); | ||
|
|
||
| return SponsorProductListResponseDto.from(brand, product, imageUrls, categories, sponsorInfoDto, action); | ||
| } | ||
|
|
||
| List<BrandAvailableSponsor> products = brandAvailableSponsorRepository.findByBrandIdWithImages(brandId); | ||
| private List<String> buildProductImageUrls(BrandAvailableSponsor product) { | ||
| List<BrandSponsorImage> images = product.getImages(); | ||
| if (images == null || images.isEmpty()) { | ||
| return List.of(); | ||
| } | ||
| return images.stream() | ||
| .map(BrandSponsorImage::getImageUrl) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| return products.stream() | ||
| .map(SponsorProductListResponseDto::from) | ||
| private List<String> buildCategories(Brand brand) { | ||
| if (brand.getIndustryType() == null) { | ||
| return List.of(); | ||
| } | ||
| return List.of(brand.getIndustryType().name()); | ||
| } | ||
|
Comment on lines
+277
to
+290
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정완료 상세 카테고리를 반환해야 하는게 맞습니다. |
||
|
|
||
| private SponsorInfoDto buildSponsorInfo(IndustryType industryType, BrandSponsorInfo sponsorInfo) { | ||
| if (sponsorInfo == null) { | ||
| return null; | ||
| } | ||
| List<SponsorItemDto> items = sponsorInfo.getItems() == null | ||
| ? List.of() | ||
| : sponsorInfo.getItems().stream() | ||
| .map(item -> { | ||
| SponsorItemDto.SponsorItemDtoBuilder builder = SponsorItemDto.builder() | ||
| .itemId(item.getId()) | ||
| .availableQuantity(item.getAvailableQuantity()); | ||
| if (industryType == IndustryType.BEAUTY) { | ||
| builder.availableType(item.getAvailableType()) | ||
| .availableSize(item.getAvailableSize()) | ||
| .sizeUnit(item.getSizeUnit()); | ||
| } | ||
| return builder.build(); | ||
| }) | ||
| .collect(Collectors.toList()); | ||
|
||
|
|
||
| return SponsorInfoDto.builder() | ||
| .items(items) | ||
| .shippingType(sponsorInfo.getShippingType()) | ||
| .build(); | ||
| } | ||
|
|
||
| private ActionDto buildAction() { | ||
| return ActionDto.builder() | ||
| .canProposeCampaign(true) | ||
| .proposeCampaignCtaText("캠페인 제안하기") | ||
| .build(); | ||
| } | ||
|
Comment on lines
+292
to
+313
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the |
||
|
|
||
| // ******** // | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package com.example.RealMatch.brand.domain.entity; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| import com.example.RealMatch.global.common.DeleteBaseEntity; | ||
|
|
||
| import jakarta.persistence.CascadeType; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.OneToMany; | ||
| import jakarta.persistence.OneToOne; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Entity | ||
| @Table(name = "brand_sponsor_info") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class BrandSponsorInfo extends DeleteBaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @OneToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "sponsor_id", nullable = false, unique = true) | ||
| private BrandAvailableSponsor sponsor; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "brand_id", nullable = false) | ||
| private Brand brand; | ||
|
|
||
| @Column(name = "shipping_type", length = 50) | ||
| private String shippingType; | ||
|
|
||
| @OneToMany(mappedBy = "sponsorInfo", fetch = FetchType.LAZY, cascade = CascadeType.ALL) | ||
| private List<BrandSponsorItem> items = new ArrayList<>(); | ||
|
|
||
| @Builder | ||
| public BrandSponsorInfo(BrandAvailableSponsor sponsor, Brand brand, String shippingType) { | ||
| this.sponsor = sponsor; | ||
| this.brand = brand; | ||
| this.shippingType = shippingType; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.example.RealMatch.brand.domain.entity; | ||
|
|
||
| import com.example.RealMatch.global.common.DeleteBaseEntity; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Entity | ||
| @Table(name = "brand_sponsor_item") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class BrandSponsorItem extends DeleteBaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "sponsor_info_id", nullable = false) | ||
| private BrandSponsorInfo sponsorInfo; | ||
|
|
||
| @Column(name = "available_type", length = 30) | ||
| private String availableType; | ||
|
|
||
| @Column(name = "available_quantity") | ||
| private Integer availableQuantity; | ||
|
|
||
| @Column(name = "available_size") | ||
| private Integer availableSize; | ||
|
|
||
| @Column(name = "size_unit", length = 20) | ||
| private String sizeUnit; | ||
|
|
||
| @Builder | ||
| public BrandSponsorItem( | ||
| BrandSponsorInfo sponsorInfo, | ||
| String availableType, | ||
| Integer availableQuantity, | ||
| Integer availableSize, | ||
| String sizeUnit | ||
| ) { | ||
| this.sponsorInfo = sponsorInfo; | ||
| this.availableType = availableType; | ||
| this.availableQuantity = availableQuantity; | ||
| this.availableSize = availableSize; | ||
| this.sizeUnit = sizeUnit; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.example.RealMatch.brand.domain.repository; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| import com.example.RealMatch.brand.domain.entity.BrandSponsorInfo; | ||
|
|
||
| public interface BrandSponsorInfoRepository extends JpaRepository<BrandSponsorInfo, Long> { | ||
|
|
||
| @Query("SELECT DISTINCT si FROM BrandSponsorInfo si LEFT JOIN FETCH si.items WHERE si.sponsor.id = :sponsorId") | ||
| Optional<BrandSponsorInfo> findBySponsorIdWithItems(@Param("sponsorId") Long sponsorId); | ||
|
|
||
| @Query("SELECT DISTINCT si FROM BrandSponsorInfo si LEFT JOIN FETCH si.items WHERE si.sponsor.id IN :sponsorIds") | ||
| List<BrandSponsorInfo> findBySponsorIdInWithItems(@Param("sponsorIds") List<Long> sponsorIds); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getSponsorProducts메소드 내에서products리스트를 순회하며buildSponsorProductListResponse를 호출하고, 이어서SponsorProductListResponseDto.from메소드가 호출됩니다. 이 과정에서product.getCampaign().getDescription()을 통해 각 제품의 캠페인 정보에 접근하게 되는데,BrandAvailableSponsor엔티티의campaign연관 필드가 지연 로딩(LAZY loading)으로 설정되어 있어 제품마다 캠페인을 조회하는 추가 쿼리가 발생하여 N+1 문제가 생깁니다.BrandAvailableSponsorRepository.findByBrandIdWithImages메소드에서campaign도 함께 fetch join 하도록 수정하여 이 문제를 해결할 수 있습니다.예를 들어,
BrandAvailableSponsorRepository에 다음과 같은 메소드를 추가하고 사용하는 것을 제안합니다:References
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영하겠습니다.