Skip to content

Commit 5eb6e47

Browse files
committed
feat/ #20 상품 데이터 정제 일부 수정
1 parent dea0c9c commit 5eb6e47

5 files changed

Lines changed: 120 additions & 39 deletions

File tree

src/main/java/com/closetnangam/be/global/external/clothes/controller/ExternalClothesController.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
import com.closetnangam.be.domain.wardrobe.entity.Wardrobe;
44
import com.closetnangam.be.domain.wardrobe.service.WardrobeService;
5+
import com.closetnangam.be.global.common.response.ApiResponse;
56
import com.closetnangam.be.global.external.clothes.dto.NaverItemRequest;
7+
import com.closetnangam.be.global.external.clothes.dto.record.SaveNaverProductRequest;
68
import com.closetnangam.be.global.external.clothes.dto.request.ClothesStyleDto;
79
import com.closetnangam.be.global.external.clothes.dto.request.ClothingColorDto;
10+
import com.closetnangam.be.global.external.clothes.dto.response.SaveNaverProductResponse;
811
import com.closetnangam.be.global.external.clothes.service.ExternalClothesService;
12+
import io.swagger.v3.oas.annotations.Operation;
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import jakarta.validation.Valid;
915
import lombok.RequiredArgsConstructor;
1016
import org.springframework.http.ResponseEntity;
1117
import org.springframework.web.bind.annotation.PostMapping;
@@ -16,25 +22,37 @@
1622

1723
import java.util.List;
1824

25+
@Tag(name = "External Clothes", description = "외부 서비스 상품 연동 API")
1926
@RestController
20-
@RequestMapping("/api/external/clothes")
27+
@RequestMapping("/api/v1/external/clothes")
2128
@RequiredArgsConstructor
2229
public class ExternalClothesController {
2330

2431
private final ExternalClothesService externalClothesService;
2532
private final WardrobeService wardrobeService;
2633

34+
@Operation(summary = "네이버 상품 저장", description = "네이버에서 검색한 상품 정보를 저장합니다.")
2735
@PostMapping("/naver")
28-
public ResponseEntity<String> saveNaverProduct(
36+
public ApiResponse<SaveNaverProductResponse> saveNaverProduct(
2937
@RequestParam Long userId,
30-
@RequestBody NaverItemRequest request,
31-
@RequestParam(required = false) List<ClothingColorDto> colorDtos, // 파라미터나 DTO 구조에 맞게 수집
32-
@RequestParam(required = false) List<ClothesStyleDto> styleDtos
38+
@Valid @RequestBody SaveNaverProductRequest request
3339
) {
34-
Wardrobe wardrobe = wardrobeService.getOrCreateWardrobe(userId);
40+
// TODO: SecurityContextHolder 통합 후 아래 검증 추가
41+
// Long authenticatedUserId = SecurityUtils.getCurrentUserId();
42+
// if (!userId.equals(authenticatedUserId)) {
43+
// throw new IllegalArgumentException("자신의 옷장에만 접근할 수 있습니다");
44+
// }
3545

36-
Long clothesId = externalClothesService.saveNaverToWishlist(userId, wardrobe, request, colorDtos, styleDtos);
46+
Wardrobe wardrobe = wardrobeService.getOrCreateWardrobe(userId);
47+
// request 내부에서 필요한 객체들만 쏙쏙 뽑아서 전달
48+
Long clothesId = externalClothesService.saveNaverToWishlist(
49+
userId,
50+
wardrobe,
51+
request, // <-- request 자체가 이제 NaverItemRequest 역할을 함!
52+
request.colors(),
53+
request.styles()
54+
);
3755

38-
return ResponseEntity.ok("External clothes saved. ID: " + clothesId);
56+
return ApiResponse.ok(new SaveNaverProductResponse(clothesId));
3957
}
4058
}
Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.closetnangam.be.global.external.clothes.dto;
22

3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
35
import lombok.AllArgsConstructor;
46
import lombok.Getter;
57
import lombok.NoArgsConstructor;
@@ -8,13 +10,29 @@
810
@NoArgsConstructor
911
@AllArgsConstructor
1012
public class NaverItemRequest {
13+
14+
@NotBlank(message = "상품명은 필수입니다")
15+
@Size(max = 255, message = "상품명은 255자 이하여야 합니다")
1116
private String title;
17+
18+
@NotBlank(message = "상품 링크는 필수입니다")
19+
@Size(max = 500, message = "링크는 500자 이하여야 합니다")
1220
private String link;
21+
22+
@NotBlank(message = "이미지 URL은 필수입니다")
23+
@Size(max = 500, message = "이미지 URL은 500자 이하여야 합니다")
1324
private String image;
14-
private String lprice;
25+
26+
@Size(max = 100)
1527
private String brand;
28+
29+
@NotBlank(message = "상품 ID는 필수입니다")
1630
private String productId;
31+
32+
@Size(max = 100)
1733
private String category1;
34+
35+
@Size(max = 100)
1836
private String category3;
1937

2038
public String getCleanTitle() {
@@ -23,4 +41,4 @@ public String getCleanTitle() {
2341
}
2442
return this.title.replaceAll("<[^>]*>", "").trim();
2543
}
26-
}
44+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.closetnangam.be.global.external.clothes.dto.record;
2+
3+
import com.closetnangam.be.global.external.clothes.dto.NaverItemRequest;
4+
import com.closetnangam.be.global.external.clothes.dto.request.ClothesStyleDto;
5+
import com.closetnangam.be.global.external.clothes.dto.request.ClothingColorDto;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
import jakarta.validation.Valid;
8+
import java.util.List;
9+
10+
public record SaveNaverProductRequest(
11+
String productId,
12+
String brand,
13+
String category3,
14+
@JsonProperty("title") String cleanTitle, // JSON의 "title" 값을 cleanTitle 변수에 넣음
15+
String image,
16+
String link,
17+
@JsonProperty("colors") List<ClothingColorDto> colors,
18+
@JsonProperty("styles") List<ClothesStyleDto> styles
19+
) {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.closetnangam.be.global.external.clothes.dto.response;
2+
3+
4+
public record SaveNaverProductResponse(Long clothesId) {
5+
}

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

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,70 +9,92 @@
99
import com.closetnangam.be.domain.clothes.repository.ClothesRepository;
1010
import com.closetnangam.be.domain.wardrobe.entity.Wardrobe;
1111
import com.closetnangam.be.global.external.clothes.dto.NaverItemRequest;
12+
import com.closetnangam.be.global.external.clothes.dto.record.SaveNaverProductRequest;
1213
import com.closetnangam.be.global.external.clothes.dto.request.ClothesStyleDto;
1314
import com.closetnangam.be.global.external.clothes.dto.request.ClothingColorDto;
1415
import lombok.RequiredArgsConstructor;
1516
import org.springframework.stereotype.Service;
1617
import org.springframework.transaction.annotation.Transactional;
18+
import org.springframework.util.StringUtils;
19+
20+
import java.util.ArrayList;
1721
import java.util.List;
1822
import java.util.Locale;
1923

2024
@Service
2125
@RequiredArgsConstructor
22-
@Transactional
26+
@Transactional(readOnly = true)
2327
public class ExternalClothesService {
2428

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";
3236

3337
private final ClothesRepository clothesRepository;
3438
private final StyleRepository styleRepository;
3539

3640

3741

38-
public Long saveNaverToWishlist(Long userId, Wardrobe wardrobe, NaverItemRequest naverRequest,
42+
@Transactional
43+
public Long saveNaverToWishlist(Long userId, Wardrobe wardrobe, SaveNaverProductRequest request,
3944
List<ClothingColorDto> colorDtos, List<ClothesStyleDto> styleDtos) {
4045

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+
4360
if (!UNKNOWN.equals(productId) && clothesRepository.existsByExternalProductId(productId)) {
4461
throw new IllegalStateException("이미 존재하는 상품입니다: " + productId);
4562
}
4663

4764
// 2. [Clothes 엔티티 생성]
65+
String brandName = StringUtils.hasText(request.brand())
66+
? request.brand().trim()
67+
: UNKNOWN;
68+
69+
String category = refineCategory(request.category3());
70+
4871
Clothes clothes = Clothes.builder()
49-
.name(naverRequest.getCleanTitle())
50-
.brandName(hasText(naverRequest.getBrand()) ? naverRequest.getBrand().trim() : UNKNOWN)
72+
.name(request.cleanTitle())
73+
.brandName(brandName)
5174
.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()))
5578
.sourceType(SourceType.WISHLIST)
5679
.externalSource("NAVER")
5780
.externalProductId(productId)
58-
.externalProductUrl(naverRequest.getLink())
81+
.externalProductUrl(request.link())
5982
.isVerified(false)
6083
.build();
61-
6284
// 3. [색상 태그 저장]
63-
for (ClothingColorDto dto : colorDtos) {
85+
for (ClothingColorDto dto : safeColors) { // colorDtos -> safeColors로 변경!
6486
ClothingColor colorTag = ClothingColor.create(
6587
clothes,
66-
dto.colorCode(), // 괄호 이름 그대로!
88+
dto.colorCode(),
6789
dto.colorRole(),
6890
dto.sortOrder()
6991
);
7092
clothes.addColorTag(colorTag);
7193
}
7294

7395
// 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())
7698
.orElseThrow(() -> new IllegalArgumentException("스타일 없음"));
7799

78100
ClothesStyleTag styleTag = ClothesStyleTag.create(
@@ -340,26 +362,25 @@ private String refineShoesItemType(String lookupText) {
340362
}
341363

342364
private String normalizeText(String value) {
343-
if (!hasText(value)) {
365+
// 2. hasText()를 직접 만든 메서드 대신 StringUtils.hasText(value) 사용
366+
if (!StringUtils.hasText(value)) {
344367
return "";
345368
}
346369
return value.toLowerCase(Locale.ROOT).replaceAll("[^\\p{IsAlphabetic}\\p{IsDigit}]+", "");
347370
}
348371

349372
private boolean containsAny(String source, String... keywords) {
373+
if (!StringUtils.hasText(source)) return false; // 소스 자체도 체크!
374+
350375
for (String keyword : keywords) {
376+
// 성능 개선: 키워드를 미리 정규화해서 상수로 뽑아두면 여기서 호출할 필요가 없어짐
351377
if (source.contains(normalizeText(keyword))) {
352378
return true;
353379
}
354380
}
355381
return false;
356382
}
357383

358-
private boolean hasText(String value) {
359-
return value != null && !value.isBlank();
360-
}
361384

362-
private String defaultIfBlank(String value, String fallback) {
363-
return hasText(value) ? value.trim() : fallback;
364-
}
385+
365386
}

0 commit comments

Comments
 (0)