Skip to content

Commit 11b2a18

Browse files
committed
feat/ #20 상품 데이터 정제 일부 수정
1 parent 99d0492 commit 11b2a18

5 files changed

Lines changed: 154 additions & 87 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//package com.closetnangam.be.domain.recommendation.service;
2+
//
3+
//import com.closetnangam.be.domain.recommendation.dto.response.RecommendResponse;
4+
//import com.closetnangam.be.global.external.naver.service.NaverApiService;
5+
//import lombok.RequiredArgsConstructor;
6+
//import org.springframework.stereotype.Component;
7+
//
8+
//import java.util.List;
9+
//import java.util.stream.Collectors;
10+
//
11+
//// 1. 네가 만든 클래스 (나중에 실제 로직을 여기에 채워!)
12+
//@Component
13+
//@RequiredArgsConstructor
14+
//public class NaverProductProvider {
15+
//
16+
// // 외부 API 서비스 (이미 네 팀에 존재하겠지?)
17+
// private final NaverApiService naverApiService;
18+
//
19+
// public List<RecommendResponse> fetchProducts(String query) {
20+
// // 여기에 네이버 API 호출 로직을 구현하면 돼!
21+
// return naverApiService.searchItems(query).stream()
22+
// .map(item -> new RecommendResponse(
23+
// item.getTitle(),
24+
// item.getLink(),
25+
// item.getImage(),
26+
// item.getLprice()))
27+
// .collect(Collectors.toList());
28+
// }
29+
//}

src/main/java/com/closetnangam/be/global/config/SecurityConfig.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ public class SecurityConfig {
2929
"/oauth2/**",
3030
"/api/v1/categories/**",
3131
"/api/categories/**", // TODO: FE /api/v1 마이그레이션 완료 후 제거 필요
32-
"/api/v1/clothes/registration-methods"
32+
"/api/v1/clothes/registration-methods",
33+
"/api/v1/external/clothes/**",
34+
"/api/naver/**",
35+
"/api/weather/**"
36+
3337
};
3438

39+
3540
private final OAuth2UserService oAuth2UserService;
3641
private final OAuth2SuccessHandler oAuth2SuccessHandler;
3742
private final JwtTokenProvider jwtTokenProvider;
@@ -53,4 +58,5 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
5358

5459
return http.build();
5560
}
61+
5662
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.closetnangam.be.global.external.clothes.dto.record;
2+
3+
import com.closetnangam.be.global.external.clothes.dto.request.ClothesStyleDto;
4+
import com.closetnangam.be.global.external.clothes.dto.request.ClothingColorDto;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
import jakarta.validation.Valid;
7+
import jakarta.validation.constraints.NotBlank;
8+
import jakarta.validation.constraints.Size;
9+
10+
import java.util.List;
11+
12+
public record NaverProductCreateRequest(
13+
@NotBlank(message = "상품 ID는 필수입니다.")
14+
String productId,
15+
16+
String brand,
17+
18+
String category3,
19+
20+
@JsonProperty("title")
21+
@NotBlank(message = "상품명은 필수입니다.")
22+
@Size(max = 255, message = "상품명은 255자 이내여야 합니다.")
23+
String cleanTitle,
24+
25+
@NotBlank(message = "이미지 URL은 필수입니다.")
26+
@Size(max = 500, message = "이미지 URL이 너무 깁니다.")
27+
String image,
28+
29+
@NotBlank(message = "상품 링크는 필수입니다.")
30+
@Size(max = 500, message = "링크 주소가 너무 깁니다.")
31+
String link,
32+
33+
@JsonProperty("colors")
34+
@Valid
35+
List<ClothingColorDto> colors,
36+
37+
@JsonProperty("styles")
38+
@Valid
39+
List<ClothesStyleDto> styles
40+
) {}

src/main/java/com/closetnangam/be/global/external/clothes/dto/record/SaveNaverProductRequest.java

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/main/java/com/closetnangam/be/global/external/clothes/service/ExternalClothesService.java

Lines changed: 78 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@
55
import com.closetnangam.be.domain.clothes.entity.Clothes;
66
import com.closetnangam.be.domain.clothes.entity.ClothesStyleTag;
77
import com.closetnangam.be.domain.clothes.entity.ClothingColor;
8+
import com.closetnangam.be.domain.clothes.entity.WardrobeClothes;
89
import com.closetnangam.be.domain.clothes.enums.SourceType;
910
import com.closetnangam.be.domain.clothes.repository.ClothesRepository;
11+
import com.closetnangam.be.domain.clothes.repository.WardrobeClothesRepository;
1012
import com.closetnangam.be.domain.wardrobe.entity.Wardrobe;
11-
import com.closetnangam.be.global.external.clothes.dto.NaverItemRequest;
12-
import com.closetnangam.be.global.external.clothes.dto.record.SaveNaverProductRequest;
13+
import com.closetnangam.be.global.external.clothes.dto.record.NaverProductCreateRequest;
1314
import com.closetnangam.be.global.external.clothes.dto.request.ClothesStyleDto;
1415
import com.closetnangam.be.global.external.clothes.dto.request.ClothingColorDto;
1516
import lombok.RequiredArgsConstructor;
1617
import org.springframework.stereotype.Service;
1718
import org.springframework.transaction.annotation.Transactional;
1819
import org.springframework.util.StringUtils;
1920

20-
import java.util.ArrayList;
21-
import java.util.List;
22-
import java.util.Locale;
21+
import java.util.*;
22+
import java.util.function.Function;
23+
import java.util.stream.Collectors;
2324

2425
@Service
2526
@RequiredArgsConstructor
@@ -36,80 +37,90 @@ public class ExternalClothesService {
3637

3738
private final ClothesRepository clothesRepository;
3839
private final StyleRepository styleRepository;
39-
40+
private final WardrobeClothesRepository wardrobeClothesRepository;
4041

4142

4243
@Transactional
43-
public Long saveNaverToWishlist(Long userId, Wardrobe wardrobe, SaveNaverProductRequest request,
44-
List<ClothingColorDto> colorDtos, List<ClothesStyleDto> styleDtos) {
44+
public Long getOrCreateExternalClothes(NaverProductCreateRequest request,
45+
List<ClothingColorDto> colorDtos,
46+
List<ClothesStyleDto> styleDtos) {
4547

4648
// 0. 안전한 리스트 처리
4749
List<ClothingColorDto> safeColors = (colorDtos != null) ? colorDtos : new ArrayList<>();
4850
List<ClothesStyleDto> safeStyles = (styleDtos != null) ? styleDtos : new ArrayList<>();
4951

50-
// 파라미터 이름이 request이므로, naverRequest를 request로 모두 변경!
5152
if (request == null) {
5253
throw new IllegalArgumentException("상품 정보가 전송되지 않았습니다.");
5354
}
5455

55-
// 1. [가드 로직] StringUtils.hasText() 활용
56-
String productId = StringUtils.hasText(request.productId())
57-
? request.productId().trim()
58-
: UNKNOWN;
59-
60-
if (!UNKNOWN.equals(productId) && clothesRepository.existsByExternalProductId(productId)) {
61-
throw new IllegalStateException("이미 존재하는 상품입니다: " + productId);
62-
}
63-
64-
// 2. [Clothes 엔티티 생성]
65-
String brandName = StringUtils.hasText(request.brand())
66-
? request.brand().trim()
67-
: UNKNOWN;
68-
69-
String category = refineCategory(request.category3());
70-
71-
Clothes clothes = Clothes.builder()
72-
.name(request.cleanTitle())
73-
.brandName(brandName)
74-
.productCode("NAVER_" + productId)
75-
.imageUrl(request.image())
76-
.category(category)
77-
.itemType(refineItemType(category, request.category3(), request.cleanTitle()))
78-
.sourceType(SourceType.WISHLIST)
79-
.externalSource("NAVER")
80-
.externalProductId(productId)
81-
.externalProductUrl(request.link())
82-
.isVerified(false)
83-
.build();
84-
// 3. [색상 태그 저장]
85-
for (ClothingColorDto dto : safeColors) { // colorDtos -> safeColors로 변경!
86-
ClothingColor colorTag = ClothingColor.create(
87-
clothes,
88-
dto.colorCode(),
89-
dto.colorRole(),
90-
dto.sortOrder()
91-
);
92-
clothes.addColorTag(colorTag);
93-
}
94-
95-
// 4. [스타일 태그 저장]
96-
for (ClothesStyleDto dto : safeStyles) { // styleDtos -> safeStyles로 변경!
97-
Style style = styleRepository.findById(dto.styleId())
98-
.orElseThrow(() -> new IllegalArgumentException("스타일 없음"));
99-
100-
ClothesStyleTag styleTag = ClothesStyleTag.create(
101-
clothes,
102-
style,
103-
dto.styleRole(),
104-
dto.sortOrder()
105-
);
106-
clothes.addStyleTag(styleTag);
107-
}
108-
109-
// 5. [Cascade 저장]
110-
// Clothes 엔티티에 @OneToMany(cascade = CascadeType.ALL)이 있으므로,
111-
// clothes만 저장해도 색상/스타일 태그가 함께 DB에 Insert됨!
112-
return clothesRepository.save(clothes).getId();
56+
// 1. 외부 상품 ID 검증 및 공백 제거
57+
String productId = StringUtils.hasText(request.productId()) ? request.productId().trim() : UNKNOWN;
58+
59+
// 2. [HTML 태그 및 품번 정제 파이프라인] - <b> 태그 박멸 및 순수 품번 추출
60+
String rawTitle = request.cleanTitle();
61+
String cleanTitle = StringUtils.hasText(rawTitle) ? rawTitle.replaceAll("<(/)?b>", "") : UNKNOWN;
62+
63+
String extractedProductCode = "NAVER_" + productId; // 기본값 세팅
64+
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\d{7,}");
65+
java.util.regex.Matcher matcher = pattern.matcher(cleanTitle);
66+
if (matcher.find()) {
67+
extractedProductCode = matcher.group(); // "1370396" 추출
68+
cleanTitle = cleanTitle.replace(extractedProductCode, "").trim(); // 이름에서 품번 제거
69+
}
70+
71+
// 3. [중복 체크] 이미 등록된 외부 상품인 경우 새로 만들지 않고 기존 옷 객체 재사용
72+
Optional<Clothes> existingClothes = clothesRepository.findByExternalProductId(productId);
73+
Clothes clothes;
74+
75+
if (existingClothes.isPresent()) {
76+
clothes = existingClothes.get();
77+
} else {
78+
// DB에 없는 새로운 상품일 때만 생성 (마스터 도감 적재)
79+
String brandName = StringUtils.hasText(request.brand()) ? request.brand().trim() : UNKNOWN;
80+
String category = refineCategory(request.category3());
81+
82+
clothes = Clothes.builder()
83+
.name(cleanTitle) // 태그와 품번이 세탁된 깔끔한 이름
84+
.brandName(brandName)
85+
.productCode(extractedProductCode)
86+
.imageUrl(request.image())
87+
.category(category)
88+
.itemType(refineItemType(category, request.category3(), cleanTitle))
89+
.sourceType(SourceType.WISHLIST)
90+
.externalSource("NAVER")
91+
.externalProductId(productId)
92+
.externalProductUrl(request.link())
93+
.isVerified(false)
94+
.build();
95+
96+
// [색상 태그 저장]
97+
for (ClothingColorDto dto : safeColors) {
98+
ClothingColor colorTag = ClothingColor.create(clothes, dto.colorCode(), dto.colorRole(), dto.sortOrder());
99+
clothes.addColorTag(colorTag);
100+
}
101+
102+
// [스타일 태그 저장]
103+
if (!safeStyles.isEmpty()) {
104+
List<Long> styleIds = safeStyles.stream().map(ClothesStyleDto::styleId).toList();
105+
Map<Long, Style> styleMap = styleRepository.findAllById(styleIds).stream()
106+
.collect(Collectors.toMap(Style::getId, Function.identity()));
107+
108+
for (ClothesStyleDto dto : safeStyles) {
109+
Style style = styleMap.get(dto.styleId());
110+
if (style == null) {
111+
throw new IllegalArgumentException("스타일 없음: " + dto.styleId());
112+
}
113+
ClothesStyleTag styleTag = ClothesStyleTag.create(clothes, style, dto.styleRole(), dto.sortOrder());
114+
clothes.addStyleTag(styleTag);
115+
}
116+
}
117+
118+
// 새로운 상품 정보 저장
119+
clothes = clothesRepository.save(clothes);
120+
}
121+
122+
// 유저 옷장에 넣는 복잡한 일은 옷장 담당자에게 맡기고, 생성/조회된 옷의 고유 ID만 깔끔하게 반환!
123+
return clothes.getId();
113124
}
114125
private String refineCategory(String naverCategory3) {
115126
String categoryText = normalizeText(naverCategory3);

0 commit comments

Comments
 (0)