55import com .closetnangam .be .domain .clothes .entity .Clothes ;
66import com .closetnangam .be .domain .clothes .entity .ClothesStyleTag ;
77import com .closetnangam .be .domain .clothes .entity .ClothingColor ;
8+ import com .closetnangam .be .domain .clothes .entity .WardrobeClothes ;
89import com .closetnangam .be .domain .clothes .enums .SourceType ;
910import com .closetnangam .be .domain .clothes .repository .ClothesRepository ;
11+ import com .closetnangam .be .domain .clothes .repository .WardrobeClothesRepository ;
1012import 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 ;
1314import com .closetnangam .be .global .external .clothes .dto .request .ClothesStyleDto ;
1415import com .closetnangam .be .global .external .clothes .dto .request .ClothingColorDto ;
1516import lombok .RequiredArgsConstructor ;
1617import org .springframework .stereotype .Service ;
1718import org .springframework .transaction .annotation .Transactional ;
1819import 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