|
9 | 9 | import com.closetnangam.be.domain.clothes.repository.ClothesRepository; |
10 | 10 | import com.closetnangam.be.domain.wardrobe.entity.Wardrobe; |
11 | 11 | import com.closetnangam.be.global.external.clothes.dto.NaverItemRequest; |
| 12 | +import com.closetnangam.be.global.external.clothes.dto.record.SaveNaverProductRequest; |
12 | 13 | import com.closetnangam.be.global.external.clothes.dto.request.ClothesStyleDto; |
13 | 14 | import com.closetnangam.be.global.external.clothes.dto.request.ClothingColorDto; |
14 | 15 | import lombok.RequiredArgsConstructor; |
15 | 16 | import org.springframework.stereotype.Service; |
16 | 17 | import org.springframework.transaction.annotation.Transactional; |
| 18 | +import org.springframework.util.StringUtils; |
| 19 | + |
| 20 | +import java.util.ArrayList; |
17 | 21 | import java.util.List; |
18 | 22 | import java.util.Locale; |
19 | 23 |
|
20 | 24 | @Service |
21 | 25 | @RequiredArgsConstructor |
22 | | -@Transactional |
| 26 | +@Transactional(readOnly = true) |
23 | 27 | public class ExternalClothesService { |
24 | 28 |
|
25 | | - private final String DEFAULT_TOP_ITEM_TYPE = "SHORT_SLEEVE"; |
26 | | - private final String DEFAULT_BOTTOM_ITEM_TYPE = "COTTON"; |
27 | | - private final String DEFAULT_CATEGORY = "TOP"; |
28 | | - private final String DEFAULT_OUTER_ITEM_TYPE = "BLAZER"; |
29 | | - private final String DEFAULT_SHOES_ITEM_TYPE = "SNEAKERS"; |
30 | | - private final String DEFAULT_COLOR = "WHITE"; |
31 | | - private final String UNKNOWN = "UNKNOWN"; |
| 29 | + private static final String DEFAULT_TOP_ITEM_TYPE = "SHORT_SLEEVE"; |
| 30 | + private static final String DEFAULT_BOTTOM_ITEM_TYPE = "COTTON"; |
| 31 | + private static final String DEFAULT_CATEGORY = "TOP"; |
| 32 | + private static final String DEFAULT_OUTER_ITEM_TYPE = "BLAZER"; |
| 33 | + private static final String DEFAULT_SHOES_ITEM_TYPE = "SNEAKERS"; |
| 34 | + private static final String DEFAULT_COLOR = "WHITE"; |
| 35 | + private static final String UNKNOWN = "UNKNOWN"; |
32 | 36 |
|
33 | 37 | private final ClothesRepository clothesRepository; |
34 | 38 | private final StyleRepository styleRepository; |
35 | 39 |
|
36 | 40 |
|
37 | 41 |
|
38 | | - public Long saveNaverToWishlist(Long userId, Wardrobe wardrobe, NaverItemRequest naverRequest, |
| 42 | + @Transactional |
| 43 | + public Long saveNaverToWishlist(Long userId, Wardrobe wardrobe, SaveNaverProductRequest request, |
39 | 44 | List<ClothingColorDto> colorDtos, List<ClothesStyleDto> styleDtos) { |
40 | 45 |
|
41 | | - // 1. [기존 가드 로직 유지] |
42 | | - String productId = defaultIfBlank(naverRequest.getProductId(), UNKNOWN); |
| 46 | + // 0. 안전한 리스트 처리 |
| 47 | + List<ClothingColorDto> safeColors = (colorDtos != null) ? colorDtos : new ArrayList<>(); |
| 48 | + List<ClothesStyleDto> safeStyles = (styleDtos != null) ? styleDtos : new ArrayList<>(); |
| 49 | + |
| 50 | + // 파라미터 이름이 request이므로, naverRequest를 request로 모두 변경! |
| 51 | + if (request == null) { |
| 52 | + throw new IllegalArgumentException("상품 정보가 전송되지 않았습니다."); |
| 53 | + } |
| 54 | + |
| 55 | + // 1. [가드 로직] StringUtils.hasText() 활용 |
| 56 | + String productId = StringUtils.hasText(request.productId()) |
| 57 | + ? request.productId().trim() |
| 58 | + : UNKNOWN; |
| 59 | + |
43 | 60 | if (!UNKNOWN.equals(productId) && clothesRepository.existsByExternalProductId(productId)) { |
44 | 61 | throw new IllegalStateException("이미 존재하는 상품입니다: " + productId); |
45 | 62 | } |
46 | 63 |
|
47 | 64 | // 2. [Clothes 엔티티 생성] |
| 65 | + String brandName = StringUtils.hasText(request.brand()) |
| 66 | + ? request.brand().trim() |
| 67 | + : UNKNOWN; |
| 68 | + |
| 69 | + String category = refineCategory(request.category3()); |
| 70 | + |
48 | 71 | Clothes clothes = Clothes.builder() |
49 | | - .name(naverRequest.getCleanTitle()) |
50 | | - .brandName(hasText(naverRequest.getBrand()) ? naverRequest.getBrand().trim() : UNKNOWN) |
| 72 | + .name(request.cleanTitle()) |
| 73 | + .brandName(brandName) |
51 | 74 | .productCode("NAVER_" + productId) |
52 | | - .imageUrl(naverRequest.getImage()) |
53 | | - .category(refineCategory(naverRequest.getCategory3())) |
54 | | - .itemType(refineItemType(refineCategory(naverRequest.getCategory3()), naverRequest.getCategory3(), naverRequest.getCleanTitle())) |
| 75 | + .imageUrl(request.image()) |
| 76 | + .category(category) |
| 77 | + .itemType(refineItemType(category, request.category3(), request.cleanTitle())) |
55 | 78 | .sourceType(SourceType.WISHLIST) |
56 | 79 | .externalSource("NAVER") |
57 | 80 | .externalProductId(productId) |
58 | | - .externalProductUrl(naverRequest.getLink()) |
| 81 | + .externalProductUrl(request.link()) |
59 | 82 | .isVerified(false) |
60 | 83 | .build(); |
61 | | - |
62 | 84 | // 3. [색상 태그 저장] |
63 | | - for (ClothingColorDto dto : colorDtos) { |
| 85 | + for (ClothingColorDto dto : safeColors) { // colorDtos -> safeColors로 변경! |
64 | 86 | ClothingColor colorTag = ClothingColor.create( |
65 | 87 | clothes, |
66 | | - dto.colorCode(), // 괄호 이름 그대로! |
| 88 | + dto.colorCode(), |
67 | 89 | dto.colorRole(), |
68 | 90 | dto.sortOrder() |
69 | 91 | ); |
70 | 92 | clothes.addColorTag(colorTag); |
71 | 93 | } |
72 | 94 |
|
73 | 95 | // 4. [스타일 태그 저장] |
74 | | - for (ClothesStyleDto dto : styleDtos) { |
75 | | - Style style = styleRepository.findById(dto.styleId()) // 여기서도 dto.styleId() |
| 96 | + for (ClothesStyleDto dto : safeStyles) { // styleDtos -> safeStyles로 변경! |
| 97 | + Style style = styleRepository.findById(dto.styleId()) |
76 | 98 | .orElseThrow(() -> new IllegalArgumentException("스타일 없음")); |
77 | 99 |
|
78 | 100 | ClothesStyleTag styleTag = ClothesStyleTag.create( |
@@ -340,26 +362,25 @@ private String refineShoesItemType(String lookupText) { |
340 | 362 | } |
341 | 363 |
|
342 | 364 | private String normalizeText(String value) { |
343 | | - if (!hasText(value)) { |
| 365 | + // 2. hasText()를 직접 만든 메서드 대신 StringUtils.hasText(value) 사용 |
| 366 | + if (!StringUtils.hasText(value)) { |
344 | 367 | return ""; |
345 | 368 | } |
346 | 369 | return value.toLowerCase(Locale.ROOT).replaceAll("[^\\p{IsAlphabetic}\\p{IsDigit}]+", ""); |
347 | 370 | } |
348 | 371 |
|
349 | 372 | private boolean containsAny(String source, String... keywords) { |
| 373 | + if (!StringUtils.hasText(source)) return false; // 소스 자체도 체크! |
| 374 | + |
350 | 375 | for (String keyword : keywords) { |
| 376 | + // 성능 개선: 키워드를 미리 정규화해서 상수로 뽑아두면 여기서 호출할 필요가 없어짐 |
351 | 377 | if (source.contains(normalizeText(keyword))) { |
352 | 378 | return true; |
353 | 379 | } |
354 | 380 | } |
355 | 381 | return false; |
356 | 382 | } |
357 | 383 |
|
358 | | - private boolean hasText(String value) { |
359 | | - return value != null && !value.isBlank(); |
360 | | - } |
361 | 384 |
|
362 | | - private String defaultIfBlank(String value, String fallback) { |
363 | | - return hasText(value) ? value.trim() : fallback; |
364 | | - } |
| 385 | + |
365 | 386 | } |
0 commit comments