Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -5,6 +5,7 @@
import lombok.RequiredArgsConstructor;
import or.sopt.houme.domain.furniture.presentation.dto.response.JjymListResponse;
import or.sopt.houme.domain.furniture.presentation.dto.response.JjymToggleResponse;
import or.sopt.houme.domain.furniture.presentation.dto.response.JjymV2ListResponse;
import or.sopt.houme.domain.furniture.service.facade.JjymOptimisticLockFacade;
import or.sopt.houme.domain.furniture.service.JjymService;
import or.sopt.houme.domain.user.presentation.controller.dto.CustomUserDetails;
Expand All @@ -14,7 +15,6 @@
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Tag(name = "찜 관련 API")
public class JjymController {
Expand All @@ -25,7 +25,7 @@ public class JjymController {


@Operation(summary = "추천 가구 찜 토글 API", description = "이미 찜이면 해제, 아니면 찜으로 저장합니다")
@PostMapping("/recommend-furnitures/{recommendFurnitureId}/jjym")
@PostMapping("/api/v1/recommend-furnitures/{recommendFurnitureId}/jjym")
public ResponseEntity<ApiResponse<JjymToggleResponse>> toggleJjym(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long recommendFurnitureId
Expand All @@ -37,11 +37,30 @@ public ResponseEntity<ApiResponse<JjymToggleResponse>> toggleJjym(

@Operation(summary = "내가 찜한 가구 목록 조회 API",
description = "찜한 가구의 이미지, 이름, 가구 식별자를 반환합니다.")
@GetMapping("/jjyms")
@GetMapping("/api/v1/jjyms")
public ResponseEntity<ApiResponse<JjymListResponse>> getMyJjyms(
@AuthenticationPrincipal CustomUserDetails userDetails
) {
JjymListResponse response = jjymService.getMyJjyms(userDetails.getUser().getId());
return ResponseEntity.ok(ApiResponse.ok(response));
}

@Operation(summary = "원천 상품 찜 토글 API v2", description = "curation_raw_product 기준으로 찜을 등록/해제합니다.")
@PostMapping("/api/v2/curation-raw-products/{rawProductId}/jjym")
public ResponseEntity<ApiResponse<JjymToggleResponse>> toggleRawProductJjym(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long rawProductId
) {
boolean favorited = jjymOptimisticLockFacade.toggleRawProduct(userDetails.getUser(), rawProductId);
return ResponseEntity.ok(ApiResponse.ok(new JjymToggleResponse(favorited)));
}

@Operation(summary = "내가 찜한 원천 상품 목록 조회 API v2", description = "찜한 원천 상품의 색상, 브랜드, 가격, 찜 개수를 포함해 반환합니다.")
@GetMapping("/api/v2/jjyms")
public ResponseEntity<ApiResponse<JjymV2ListResponse>> getMyRawProductJjyms(
@AuthenticationPrincipal CustomUserDetails userDetails
) {
JjymV2ListResponse response = jjymService.getMyRawProductJjyms(userDetails.getUser().getId());
return ResponseEntity.ok(ApiResponse.ok(response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package or.sopt.houme.domain.furniture.presentation.dto.response;

import java.util.List;

public record JjymV2ItemResponse(
Long rawProductId,
boolean isJjym,
String productImageUrl,
String productSiteUrl,
List<String> colors,
String brandName,
String productName,
Long listPrice,
Integer discountRate,
Long discountPrice,
Long jjymCount
) {
public static JjymV2ItemResponse of(
Long rawProductId,
boolean isJjym,
String productImageUrl,
String productSiteUrl,
List<String> colors,
String brandName,
String productName,
Long listPrice,
Integer discountRate,
Long discountPrice,
Long jjymCount
) {
return new JjymV2ItemResponse(
rawProductId,
isJjym,
productImageUrl,
productSiteUrl,
colors,
brandName,
productName,
listPrice,
discountRate,
discountPrice,
jjymCount
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package or.sopt.houme.domain.furniture.presentation.dto.response;

import java.util.List;

public record JjymV2ListResponse(List<JjymV2ItemResponse> items) {
public static JjymV2ListResponse of(List<JjymV2ItemResponse> items) {
return new JjymV2ListResponse(items);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ List<CurationRawProduct> findAllBySourceAndCategoryAndProductIdIn(
List<Long> productIds
);

List<CurationRawProduct> findAllByProductIdIn(List<Long> productIds);

@Query("""
select distinct rawProduct
from CurationRawProduct rawProduct
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package or.sopt.houme.domain.furniture.service;

import or.sopt.houme.domain.furniture.presentation.dto.response.JjymListResponse;
import or.sopt.houme.domain.furniture.presentation.dto.response.JjymV2ListResponse;

public interface JjymService {
boolean jjymToggle(Long userId, Long recommendFurnitureId);

boolean rawProductJjymToggle(Long userId, Long rawProductId);

JjymListResponse getMyJjyms(Long userId);

JjymV2ListResponse getMyRawProductJjyms(Long userId);
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
package or.sopt.houme.domain.furniture.service;

import lombok.RequiredArgsConstructor;
import or.sopt.houme.domain.furniture.model.entity.CurationRawProduct;
import or.sopt.houme.domain.furniture.model.entity.CurationRawProductColor;
import or.sopt.houme.domain.furniture.model.entity.CurationSource;
import or.sopt.houme.domain.furniture.model.entity.Jjym;
import or.sopt.houme.domain.furniture.model.entity.RecommendFurniture;
import or.sopt.houme.domain.furniture.presentation.dto.response.JjymItemResponse;
import or.sopt.houme.domain.furniture.presentation.dto.response.JjymListResponse;
import or.sopt.houme.domain.furniture.presentation.dto.response.JjymV2ItemResponse;
import or.sopt.houme.domain.furniture.presentation.dto.response.JjymV2ListResponse;
import or.sopt.houme.domain.furniture.repository.CurationRawProductColorRepository;
import or.sopt.houme.domain.furniture.repository.CurationRawProductRepository;
import or.sopt.houme.domain.furniture.repository.JjymRepository;
import or.sopt.houme.domain.furniture.repository.RecommendFurnitureRepository;
import or.sopt.houme.domain.user.model.entity.User;
import or.sopt.houme.domain.user.repository.UserRepository;
import or.sopt.houme.global.api.ErrorCode;
import or.sopt.houme.global.api.GeneralException;
import or.sopt.houme.global.api.handler.FurnitureException;
import or.sopt.houme.global.api.handler.UserException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Service
Expand All @@ -26,11 +41,13 @@ public class JjymServiceImpl implements JjymService {
private final JjymRepository jjymRepository;
private final UserRepository userRepository;
private final RecommendFurnitureRepository recommendFurnitureRepository;
private final CurationRawProductRepository curationRawProductRepository;
private final CurationRawProductColorRepository curationRawProductColorRepository;

@Override
public boolean jjymToggle(Long userId, Long recommendFurnitureId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(ErrorCode.USER_NOT_FOUND));
.orElseThrow(() -> new UserException(ErrorCode.USER_NOT_FOUND));

RecommendFurniture furniture = recommendFurnitureRepository.findById(recommendFurnitureId)
.orElseThrow(() -> new GeneralException(ErrorCode.NOT_FOUND_FURNITURE));
Expand All @@ -47,6 +64,35 @@ public boolean jjymToggle(Long userId, Long recommendFurnitureId) {
}
}

@Override
public boolean rawProductJjymToggle(Long userId, Long rawProductId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserException(ErrorCode.USER_NOT_FOUND));

CurationRawProduct rawProduct = curationRawProductRepository.findById(rawProductId)
.orElseThrow(() -> new FurnitureException(ErrorCode.NOT_FOUND_CURATION_RAW_PRODUCT));

RecommendFurniture recommendFurniture = recommendFurnitureRepository
.findBySourceAndFurnitureProductId(CurationSource.RAW, rawProduct.getProductId())
.orElseGet(() -> recommendFurnitureRepository.save(RecommendFurniture.from(
rawProduct.getProductImageUrl(),
rawProduct.getProductSiteUrl(),
rawProduct.getProductName(),
rawProduct.getProductMallName(),
rawProduct.getProductId(),
CurationSource.RAW
)));

Optional<Jjym> existing = jjymRepository.findByUserIdAndRecommendFurnitureId(user.getId(), recommendFurniture.getId());
if (existing.isPresent()) {
jjymRepository.delete(existing.get());
return false;
}

jjymRepository.save(Jjym.of(user, recommendFurniture));
return true;
}

@Transactional(readOnly = true)
@Override
public JjymListResponse getMyJjyms(Long userId) {
Expand All @@ -60,4 +106,161 @@ public JjymListResponse getMyJjyms(Long userId) {
return JjymListResponse.of(items);

}

@Transactional(readOnly = true)
@Override
public JjymV2ListResponse getMyRawProductJjyms(Long userId) {
List<Jjym> rawProductJjyms = jjymRepository.findAllByUserIdWithFurnitureOrderByCreatedAtDesc(userId).stream()
.filter(jjym -> jjym.getRecommendFurniture().getSource() == CurationSource.RAW)
.toList();

if (rawProductJjyms.isEmpty()) {
return JjymV2ListResponse.of(List.of());
}

Map<Long, CurationRawProduct> rawProductByProductId = buildRawProductByProductId(rawProductJjyms);
Map<Long, List<String>> colorsByRawProductId = buildColorNamesByRawProductId(rawProductByProductId);
Map<Long, Long> jjymCountByRecommendFurnitureId = jjymRepository.countByRecommendFurnitureIds(
rawProductJjyms.stream()
.map(jjym -> jjym.getRecommendFurniture().getId())
.distinct()
.toList()
);

List<JjymV2ItemResponse> items = rawProductJjyms.stream()
.map(jjym -> toV2ItemResponse(jjym, rawProductByProductId, colorsByRawProductId, jjymCountByRecommendFurnitureId))
.toList();

return JjymV2ListResponse.of(items);
}

private Map<Long, CurationRawProduct> buildRawProductByProductId(List<Jjym> rawProductJjyms) {
List<Long> productIds = rawProductJjyms.stream()
.map(jjym -> jjym.getRecommendFurniture().getFurnitureProductId())
.filter(productId -> productId != null)
.distinct()
.toList();

if (productIds.isEmpty()) {
return Map.of();
}

Map<Long, CurationRawProduct> rawProductByProductId = new HashMap<>();
for (CurationRawProduct rawProduct : curationRawProductRepository.findAllByProductIdIn(productIds)) {
rawProductByProductId.merge(
rawProduct.getProductId(),
rawProduct,
this::selectLatestRawProduct
);
}
return rawProductByProductId;
}

private Map<Long, List<String>> buildColorNamesByRawProductId(Map<Long, CurationRawProduct> rawProductByProductId) {
if (rawProductByProductId.isEmpty()) {
return Map.of();
}

List<Long> rawProductIds = rawProductByProductId.values().stream()
.map(CurationRawProduct::getId)
.toList();

Map<Long, Set<String>> colorSetByRawProductId = new HashMap<>();
for (CurationRawProductColor color : curationRawProductColorRepository.findAllByCurationRawProductIdIn(rawProductIds)) {
Long rawProductId = color.getCurationRawProduct().getId();
if (rawProductId == null) {
continue;
}

String colorName = resolveColorName(color);
if (colorName == null) {
continue;
}

colorSetByRawProductId.computeIfAbsent(rawProductId, key -> new LinkedHashSet<>()).add(colorName);
}

Map<Long, List<String>> colorsByRawProductId = new HashMap<>();
for (Map.Entry<Long, Set<String>> entry : colorSetByRawProductId.entrySet()) {
colorsByRawProductId.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
return colorsByRawProductId;
}

private JjymV2ItemResponse toV2ItemResponse(
Jjym jjym,
Map<Long, CurationRawProduct> rawProductByProductId,
Map<Long, List<String>> colorsByRawProductId,
Map<Long, Long> jjymCountByRecommendFurnitureId
) {
RecommendFurniture recommendFurniture = jjym.getRecommendFurniture();
CurationRawProduct rawProduct = rawProductByProductId.get(recommendFurniture.getFurnitureProductId());

if (rawProduct == null) {
return JjymV2ItemResponse.of(
null,
true,
recommendFurniture.getFurnitureProductImageUrl(),
recommendFurniture.getFurnitureProductSiteUrl(),
List.of(),
null,
recommendFurniture.getFurnitureProductName(),
null,
null,
null,
jjymCountByRecommendFurnitureId.getOrDefault(recommendFurniture.getId(), 0L)
);
}

return JjymV2ItemResponse.of(
rawProduct.getId(),
true,
rawProduct.getProductImageUrl(),
rawProduct.getProductSiteUrl(),
colorsByRawProductId.getOrDefault(rawProduct.getId(), List.of()),
rawProduct.getBrand(),
rawProduct.getProductName(),
rawProduct.getListPrice(),
rawProduct.getDiscountRate(),
rawProduct.getDiscountPrice(),
jjymCountByRecommendFurnitureId.getOrDefault(recommendFurniture.getId(), 0L)
);
}

private CurationRawProduct selectLatestRawProduct(CurationRawProduct current, CurationRawProduct candidate) {
LocalDateTime currentFetchedAt = current.getFetchedAt();
LocalDateTime candidateFetchedAt = candidate.getFetchedAt();

if (currentFetchedAt == null && candidateFetchedAt == null) {
return current.getId() != null && candidate.getId() != null && candidate.getId() > current.getId()
? candidate
: current;
}
if (currentFetchedAt == null) {
return candidate;
}
if (candidateFetchedAt == null) {
return current;
}
if (candidateFetchedAt.isAfter(currentFetchedAt)) {
return candidate;
}
if (candidateFetchedAt.isEqual(currentFetchedAt)
&& current.getId() != null
&& candidate.getId() != null
&& candidate.getId() > current.getId()) {
return candidate;
}
return current;
}

private String resolveColorName(CurationRawProductColor color) {
if (color.getClientColorName() != null && !color.getClientColorName().isBlank()) {
return color.getClientColorName();
}
if (color.getRawColorName() != null && !color.getRawColorName().isBlank()) {
return color.getRawColorName();
}
return null;
}
}
Loading
Loading