Skip to content

Commit 28d84af

Browse files
authored
Merge pull request #44 from prgrms-aibe-devcourse/feat/#43
feat/ #43 enum추가, 외부 출처 추가
2 parents c2b3e23 + f0af276 commit 28d84af

17 files changed

Lines changed: 287 additions & 32 deletions

src/main/java/com/closetnangam/be/domain/catalog/controller/CategoryController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.closetnangam.be.domain.catalog.dto.response.CategoryCatalogResponse;
44
import com.closetnangam.be.domain.catalog.dto.response.CategoryUsageGuideResponse;
5+
import com.closetnangam.be.domain.catalog.dto.response.ExternalSourcesResponse;
56
import com.closetnangam.be.domain.catalog.service.CategoryCatalogService;
67
import com.closetnangam.be.global.common.response.ApiResponse;
78
import io.swagger.v3.oas.annotations.Operation;
@@ -51,4 +52,17 @@ public ResponseEntity<ApiResponse<CategoryUsageGuideResponse>> getUsageGuide() {
5152
public ResponseEntity<ApiResponse<String>> getAiGuide() {
5253
return ResponseEntity.ok(ApiResponse.ok(categoryCatalogService.getAiClassificationGuide()));
5354
}
55+
56+
@Operation(
57+
summary = "외부 쇼핑 출처 목록 조회",
58+
description = """
59+
미보유 옷 저장 시 선택할 외부 출처 12개 + 직접입력 옵션을 반환합니다.
60+
- 선택 항목: externalSource에 code 저장 (예: NAVER_SHOPPING, MUSINSA)
61+
- 직접입력: allowsCustomInput=true 항목 선택 후 사용자 입력값을 externalSource에 저장
62+
"""
63+
)
64+
@GetMapping("/external-sources")
65+
public ResponseEntity<ApiResponse<ExternalSourcesResponse>> getExternalSources() {
66+
return ResponseEntity.ok(ApiResponse.ok(categoryCatalogService.getExternalSources()));
67+
}
5468
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.closetnangam.be.domain.catalog.dto.response;
2+
3+
import com.closetnangam.be.domain.catalog.enums.ExternalSource;
4+
import com.closetnangam.be.domain.catalog.enums.ExternalSourceGroup;
5+
6+
public record ExternalSourceResponse(
7+
String code,
8+
String label,
9+
ExternalSourceGroup group,
10+
String groupLabel,
11+
boolean allowsCustomInput
12+
) {
13+
14+
public static ExternalSourceResponse from(ExternalSource source) {
15+
return new ExternalSourceResponse(
16+
source.name(),
17+
source.getLabel(),
18+
source.getGroup(),
19+
source.getGroup().getLabel(),
20+
source.isAllowsCustomInput()
21+
);
22+
}
23+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.closetnangam.be.domain.catalog.dto.response;
2+
3+
import java.util.List;
4+
5+
public record ExternalSourcesResponse(
6+
String description,
7+
List<ExternalSourceResponse> sources
8+
) {
9+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.closetnangam.be.domain.catalog.enums;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
import java.util.Arrays;
7+
import java.util.Optional;
8+
9+
@Getter
10+
@RequiredArgsConstructor
11+
public enum ExternalSource {
12+
13+
NAVER_SHOPPING("네이버쇼핑", ExternalSourceGroup.OPEN_MARKET, false),
14+
COUPANG("쿠팡", ExternalSourceGroup.OPEN_MARKET, false),
15+
MUSINSA("무신사", ExternalSourceGroup.FASHION_PLATFORM, false),
16+
ABLY("에이블리", ExternalSourceGroup.FASHION_PLATFORM, false),
17+
ZIGZAG("지그재그", ExternalSourceGroup.FASHION_PLATFORM, false),
18+
TWENTYNINE_CM("29CM", ExternalSourceGroup.FASHION_PLATFORM, false),
19+
WCONCEPT("W컨셉", ExternalSourceGroup.FASHION_PLATFORM, false),
20+
BRANDI("브랜디", ExternalSourceGroup.FASHION_PLATFORM, false),
21+
UNIQLO("유니클로", ExternalSourceGroup.SPA, false),
22+
SPAO("스파오", ExternalSourceGroup.SPA, false),
23+
EIGHT_SECONDS("에잇세컨즈", ExternalSourceGroup.SPA, false),
24+
HM("H&M", ExternalSourceGroup.SPA, false),
25+
CUSTOM("직접입력", ExternalSourceGroup.CUSTOM, true);
26+
27+
private final String label;
28+
private final ExternalSourceGroup group;
29+
private final boolean allowsCustomInput;
30+
31+
public static ExternalSource fromCode(String code) {
32+
return Arrays.stream(values())
33+
.filter(source -> source.name().equals(code))
34+
.findFirst()
35+
.orElseThrow(() -> new IllegalArgumentException("유효하지 않은 외부 출처 코드입니다: " + code));
36+
}
37+
38+
public static Optional<ExternalSource> findByCode(String code) {
39+
return Arrays.stream(values())
40+
.filter(source -> source.name().equals(code))
41+
.findFirst();
42+
}
43+
44+
public static String resolveDisplayName(String storedValue) {
45+
if (storedValue == null) {
46+
return null;
47+
}
48+
return findByCode(storedValue)
49+
.map(ExternalSource::getLabel)
50+
.orElse(storedValue);
51+
}
52+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.closetnangam.be.domain.catalog.enums;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
@Getter
7+
@RequiredArgsConstructor
8+
public enum ExternalSourceGroup {
9+
10+
OPEN_MARKET("플랫폼"),
11+
FASHION_PLATFORM("플랫폼 및 SPA"),
12+
SPA("SPA"),
13+
CUSTOM("직접입력");
14+
15+
private final String label;
16+
}

src/main/java/com/closetnangam/be/domain/catalog/service/CategoryCatalogService.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import com.closetnangam.be.domain.catalog.enums.ClothesCategory;
77
import com.closetnangam.be.domain.catalog.enums.ClothesColor;
88
import com.closetnangam.be.domain.catalog.enums.ClothesItemType;
9+
import com.closetnangam.be.domain.catalog.enums.ExternalSource;
910
import com.closetnangam.be.domain.catalog.enums.StyleCode;
1011
import com.closetnangam.be.domain.catalog.repository.StyleRepository;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.stereotype.Service;
1314
import org.springframework.transaction.annotation.Transactional;
15+
import org.springframework.util.StringUtils;
1416

1517
import java.util.Arrays;
1618
import java.util.HashSet;
@@ -201,6 +203,38 @@ public void validateStyleCodes(List<String> styleCodes) {
201203
}
202204
}
203205

206+
public ExternalSourcesResponse getExternalSources() {
207+
return new ExternalSourcesResponse(
208+
"""
209+
미보유 옷 저장 시 사용할 외부 쇼핑 출처 목록입니다.
210+
- 목록에서 선택: externalSource에 code 값(예: MUSINSA)을 저장합니다.
211+
- 직접입력: 사용자가 입력한 출처명을 externalSource VARCHAR에 그대로 저장합니다.
212+
DB 컬럼은 enum이 아닌 VARCHAR(50)입니다.
213+
""",
214+
Arrays.stream(ExternalSource.values())
215+
.map(ExternalSourceResponse::from)
216+
.toList()
217+
);
218+
}
219+
220+
public void validateExternalSource(String externalSource) {
221+
if (!StringUtils.hasText(externalSource)) {
222+
throw new IllegalArgumentException("외부 출처는 필수입니다.");
223+
}
224+
if (externalSource.length() > 50) {
225+
throw new IllegalArgumentException("외부 출처는 50자 이하여야 합니다.");
226+
}
227+
if ("NONE".equals(externalSource)) {
228+
throw new IllegalArgumentException("외부 출처를 선택하거나 입력해 주세요.");
229+
}
230+
231+
ExternalSource.findByCode(externalSource).ifPresent(matched -> {
232+
if (matched.isAllowsCustomInput()) {
233+
throw new IllegalArgumentException("직접입력 출처명을 입력해 주세요.");
234+
}
235+
});
236+
}
237+
204238
private java.util.List<CategoryGroupResponse> getCategoryGroups() {
205239
return Arrays.stream(ClothesCategory.values())
206240
.map(category -> new CategoryGroupResponse(

src/main/java/com/closetnangam/be/domain/clothes/dto/response/ClothesResponse.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.closetnangam.be.domain.clothes.entity.ClothingColor;
66
import com.closetnangam.be.domain.clothes.entity.WardrobeClothes;
77
import com.closetnangam.be.domain.clothes.enums.ColorRole;
8+
import com.closetnangam.be.domain.clothes.enums.ClothesInfoSource;
89
import com.closetnangam.be.domain.clothes.enums.SourceType;
910

1011
import java.time.LocalDateTime;
@@ -26,6 +27,7 @@ public record ClothesResponse(
2627
List<SecondaryColorResponse> secondaryColors,
2728
List<StyleTagResponse> styles,
2829
SourceType sourceType,
30+
ClothesInfoSource infoSource,
2931
String externalSource,
3032
String externalProductId,
3133
String externalProductUrl,
@@ -88,6 +90,7 @@ public static ClothesResponse from(Clothes clothes, WardrobeClothes wardrobeClot
8890
secondaryColors,
8991
styles,
9092
clothes.getSourceType(),
93+
clothes.getInfoSource(),
9194
clothes.getExternalSource(),
9295
clothes.getExternalProductId(),
9396
clothes.getExternalProductUrl(),

src/main/java/com/closetnangam/be/domain/clothes/dto/response/PhotoClothesRegistrationResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package com.closetnangam.be.domain.clothes.dto.response;
22

3+
import com.closetnangam.be.domain.clothes.enums.ClothesInfoSource;
34
import com.closetnangam.be.domain.clothes.enums.OwnershipStatus;
4-
import com.closetnangam.be.domain.clothes.enums.RegistrationSource;
55

66
public record PhotoClothesRegistrationResponse(
77
Long wardrobeClothesId,
88
Long clothesId,
99
Long photoId,
1010
String userImageUrl,
1111
OwnershipStatus ownershipStatus,
12-
RegistrationSource registrationSource,
12+
ClothesInfoSource infoSource,
1313
String size,
1414
String season,
1515
Boolean favorite,

src/main/java/com/closetnangam/be/domain/clothes/entity/Clothes.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.closetnangam.be.domain.clothes.entity;
22

3+
import com.closetnangam.be.domain.clothes.enums.ClothesInfoSource;
34
import com.closetnangam.be.domain.clothes.enums.SourceType;
45
import com.closetnangam.be.global.common.entity.BaseEntity;
56
import jakarta.persistence.CascadeType;
@@ -31,7 +32,7 @@
3132
@Table(name = "clothes")
3233
public class Clothes extends BaseEntity {
3334

34-
private static final String EXTERNAL_NONE = "NONE";
35+
public static final String EXTERNAL_NONE = "NONE";
3536

3637
@Id
3738
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -60,6 +61,10 @@ public class Clothes extends BaseEntity {
6061
@Column(name = "source_type", nullable = false, length = 50)
6162
private SourceType sourceType;
6263

64+
@Enumerated(EnumType.STRING)
65+
@Column(name = "info_source", nullable = false, length = 50)
66+
private ClothesInfoSource infoSource;
67+
6368
@Column(name = "external_source", nullable = false, length = 50)
6469
private String externalSource;
6570

@@ -95,6 +100,7 @@ private Clothes(
95100
String category,
96101
String itemType,
97102
SourceType sourceType,
103+
ClothesInfoSource infoSource,
98104
String externalSource,
99105
String externalProductId,
100106
String externalProductUrl,
@@ -107,6 +113,10 @@ private Clothes(
107113
this.category = category;
108114
this.itemType = itemType;
109115
this.sourceType = sourceType;
116+
if (infoSource == null) {
117+
throw new IllegalArgumentException("옷 정보 출처는 필수입니다.");
118+
}
119+
this.infoSource = infoSource;
110120
this.externalSource = externalSource;
111121
this.externalProductId = externalProductId;
112122
this.externalProductUrl = externalProductUrl;
@@ -166,6 +176,7 @@ public void convertToOwned(String productCode, Boolean isVerified) {
166176
throw new IllegalArgumentException("미보유 옷만 보유 옷으로 전환할 수 있습니다.");
167177
}
168178
this.sourceType = SourceType.OWNED;
179+
this.infoSource = ClothesInfoSource.PURCHASE_HISTORY;
169180
this.externalSource = EXTERNAL_NONE;
170181
this.externalProductId = EXTERNAL_NONE;
171182
this.externalProductUrl = EXTERNAL_NONE;

src/main/java/com/closetnangam/be/domain/clothes/entity/WardrobeClothes.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.closetnangam.be.domain.clothes.entity;
22

3+
import com.closetnangam.be.domain.clothes.enums.ClothesInfoSource;
34
import com.closetnangam.be.domain.clothes.enums.OwnershipStatus;
4-
import com.closetnangam.be.domain.clothes.enums.RegistrationSource;
55
import com.closetnangam.be.domain.wardrobe.entity.Wardrobe;
66
import com.closetnangam.be.global.common.entity.BaseEntity;
77
import jakarta.persistence.Column;
@@ -52,9 +52,10 @@ public class WardrobeClothes extends BaseEntity {
5252
@Column(nullable = false)
5353
private Boolean favorite = false;
5454

55+
/** clothes.info_source와 동기화되는 비정규화 컬럼(레거시). 빌더에서 clothes 기준으로만 설정합니다. */
5556
@Enumerated(EnumType.STRING)
5657
@Column(name = "registration_source", nullable = false, length = 50)
57-
private RegistrationSource registrationSource;
58+
private ClothesInfoSource registrationSource;
5859

5960
@Column(name = "user_image_url", nullable = false, length = 500)
6061
private String userImageUrl;
@@ -67,7 +68,6 @@ private WardrobeClothes(
6768
String size,
6869
String season,
6970
Boolean favorite,
70-
RegistrationSource registrationSource,
7171
String userImageUrl
7272
) {
7373
this.wardrobe = wardrobe;
@@ -76,8 +76,11 @@ private WardrobeClothes(
7676
this.size = size;
7777
this.season = season;
7878
this.favorite = favorite != null ? favorite : false;
79-
this.registrationSource = registrationSource;
8079
this.userImageUrl = userImageUrl;
80+
if (clothes.getInfoSource() == null) {
81+
throw new IllegalArgumentException("옷 정보 출처가 설정되지 않았습니다.");
82+
}
83+
this.registrationSource = clothes.getInfoSource();
8184
}
8285

8386
public void updateFavorite(Boolean favorite) {
@@ -95,5 +98,6 @@ public void convertToOwned(String size, String season, String userImageUrl) {
9598
this.size = size;
9699
this.season = season;
97100
this.userImageUrl = userImageUrl;
101+
this.registrationSource = this.clothes.getInfoSource();
98102
}
99103
}

0 commit comments

Comments
 (0)