Skip to content

Commit e5212aa

Browse files
committed
feat/#22-clothes-crud
1 parent 5263003 commit e5212aa

6 files changed

Lines changed: 162 additions & 4 deletions

File tree

src/main/java/com/closetnangam/be/domain/clothes/controller/ClothesController.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public ResponseEntity<ApiResponse<List<ClothesResponse>>> getFavoriteOwnedClothe
4343
return ResponseEntity.ok(ApiResponse.ok(clothesService.getFavoriteOwnedClothes(userId)));
4444
}
4545

46-
@Operation(summary = "옷 상세 조회", description = "이미지, 이름, 브랜드, 카테고리, 타입, 색상, 스타일 정보를 조회합니다.")
46+
@Operation(summary = "옷 상세 조회", description = "보유/미보유 옷의 상세 정보(이미지, 이름, 브랜드, 카테고리, 타입, 색상, 스타일)를 조회합니다.")
4747
@GetMapping("/api/clothes/{clothesId}")
4848
public ResponseEntity<ApiResponse<ClothesResponse>> getClothes(@PathVariable Long clothesId) {
4949
return ResponseEntity.ok(ApiResponse.ok(clothesService.getClothes(clothesId)));
@@ -60,7 +60,7 @@ public ResponseEntity<ApiResponse<ClothesResponse>> createOwnedClothes(
6060
.body(ApiResponse.ok(clothesService.createOwnedClothes(userId, request)));
6161
}
6262

63-
@Operation(summary = "옷 즐겨찾기 설정", description = "옷의 즐겨찾기 상태를 등록/해제합니다.")
63+
@Operation(summary = "옷 즐겨찾기 설정", description = "보유/미보유 옷의 즐겨찾기 상태를 등록/해제합니다.")
6464
@PatchMapping("/api/clothes/{clothesId}/favorite")
6565
public ResponseEntity<ApiResponse<ClothesResponse>> updateFavorite(
6666
@PathVariable Long clothesId,
@@ -69,7 +69,7 @@ public ResponseEntity<ApiResponse<ClothesResponse>> updateFavorite(
6969
return ResponseEntity.ok(ApiResponse.ok(clothesService.updateFavorite(clothesId, request)));
7070
}
7171

72-
@Operation(summary = "옷 정보 수정", description = "등록된 보유 옷 정보를 수정합니다.")
72+
@Operation(summary = "옷 정보 수정", description = "등록된 보유/미보유 옷 정보를 수정합니다.")
7373
@PatchMapping("/api/clothes/{clothesId}")
7474
public ResponseEntity<ApiResponse<ClothesResponse>> updateClothes(
7575
@PathVariable Long clothesId,
@@ -78,7 +78,7 @@ public ResponseEntity<ApiResponse<ClothesResponse>> updateClothes(
7878
return ResponseEntity.ok(ApiResponse.ok(clothesService.updateClothes(clothesId, request)));
7979
}
8080

81-
@Operation(summary = "옷 삭제", description = "등록된 보유 옷을 삭제합니다. 삭제 확인은 프론트에서 처리합니다.")
81+
@Operation(summary = "옷 삭제", description = "등록된 보유/미보유 옷을 삭제합니다. 삭제 확인은 프론트에서 처리합니다.")
8282
@DeleteMapping("/api/clothes/{clothesId}")
8383
@ResponseStatus(HttpStatus.NO_CONTENT)
8484
public ResponseEntity<Void> deleteClothes(@PathVariable Long clothesId) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.closetnangam.be.domain.clothes.controller;
2+
3+
import com.closetnangam.be.domain.clothes.dto.request.ClothesConvertToOwnedRequest;
4+
import com.closetnangam.be.domain.clothes.dto.request.WishlistClothesCreateRequest;
5+
import com.closetnangam.be.domain.clothes.dto.response.ClothesResponse;
6+
import com.closetnangam.be.domain.clothes.service.ClothesService;
7+
import com.closetnangam.be.global.common.response.ApiResponse;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.validation.Valid;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.http.HttpStatus;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.web.bind.annotation.GetMapping;
15+
import org.springframework.web.bind.annotation.PatchMapping;
16+
import org.springframework.web.bind.annotation.PathVariable;
17+
import org.springframework.web.bind.annotation.PostMapping;
18+
import org.springframework.web.bind.annotation.RequestBody;
19+
import org.springframework.web.bind.annotation.RestController;
20+
21+
import java.util.List;
22+
23+
@Tag(name = "Wishlist Clothes", description = "미보유 옷(WISHLIST) CRUD API")
24+
@RestController
25+
@RequiredArgsConstructor
26+
public class WishlistClothesController {
27+
28+
private final ClothesService clothesService;
29+
30+
// TODO: JWT 인증 구현 후 @PreAuthorize 또는 SecurityContextHolder로 userId 소유권 검증 추가 필요
31+
@Operation(summary = "미보유 옷 목록 조회", description = "추천받아 저장한 옷, 관심 상품(WISHLIST) 목록을 조회합니다.")
32+
@GetMapping("/api/users/{userId}/wishlist-clothes")
33+
public ResponseEntity<ApiResponse<List<ClothesResponse>>> getWishlistClothes(@PathVariable Long userId) {
34+
return ResponseEntity.ok(ApiResponse.ok(clothesService.getWishlistClothes(userId)));
35+
}
36+
37+
@Operation(summary = "미보유 옷 즐겨찾기 목록 조회", description = "즐겨찾기로 표시한 미보유 옷 목록을 조회합니다.")
38+
@GetMapping("/api/users/{userId}/wishlist-clothes/favorites")
39+
public ResponseEntity<ApiResponse<List<ClothesResponse>>> getFavoriteWishlistClothes(@PathVariable Long userId) {
40+
return ResponseEntity.ok(ApiResponse.ok(clothesService.getFavoriteWishlistClothes(userId)));
41+
}
42+
43+
@Operation(summary = "미보유 옷 등록", description = "추천 상품 또는 외부 쇼핑 상품을 미보유 옷으로 저장합니다.")
44+
@PostMapping("/api/users/{userId}/wishlist-clothes")
45+
public ResponseEntity<ApiResponse<ClothesResponse>> createWishlistClothes(
46+
@PathVariable Long userId,
47+
@Valid @RequestBody WishlistClothesCreateRequest request
48+
) {
49+
return ResponseEntity.status(HttpStatus.CREATED)
50+
.body(ApiResponse.ok(clothesService.createWishlistClothes(userId, request)));
51+
}
52+
53+
// TODO: JWT 인증 구현 후 SecurityContextHolder로 clothesId 소유권 검증 추가 필요
54+
@Operation(summary = "미보유 → 보유 전환", description = "구매 후 미보유 옷을 보유 옷(OWNED)으로 전환합니다.")
55+
@PatchMapping("/api/clothes/{clothesId}/convert-to-owned")
56+
public ResponseEntity<ApiResponse<ClothesResponse>> convertToOwned(
57+
@PathVariable Long clothesId,
58+
@Valid @RequestBody ClothesConvertToOwnedRequest request
59+
) {
60+
return ResponseEntity.ok(ApiResponse.ok(clothesService.convertToOwned(clothesId, request)));
61+
}
62+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.closetnangam.be.domain.clothes.dto.request;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.Size;
6+
7+
public record ClothesConvertToOwnedRequest(
8+
@NotBlank @Size(max = 100) String productCode,
9+
@NotNull Boolean isVerified
10+
) {
11+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.closetnangam.be.domain.clothes.dto.request;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotEmpty;
5+
import jakarta.validation.constraints.Size;
6+
7+
import java.util.List;
8+
9+
public record WishlistClothesCreateRequest(
10+
@NotBlank @Size(max = 255) String name,
11+
@NotBlank @Size(max = 100) String brandName,
12+
@NotBlank @Size(max = 100) String productCode,
13+
@NotBlank @Size(max = 500) String imageUrl,
14+
@NotBlank @Size(max = 50) String category,
15+
@NotBlank @Size(max = 50) String itemType,
16+
@NotBlank @Size(max = 50) String color,
17+
@NotEmpty List<@NotBlank String> styles,
18+
@NotBlank @Size(max = 50) String externalSource,
19+
@NotBlank @Size(max = 255) String externalProductId,
20+
@NotBlank @Size(max = 500) String externalProductUrl
21+
) {
22+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
@Table(name = "clothes")
3434
public class Clothes extends BaseEntity {
3535

36+
private static final String EXTERNAL_NONE = "NONE";
37+
3638
@Id
3739
@GeneratedValue(strategy = GenerationType.IDENTITY)
3840
@Column(name = "clothes_id")
@@ -161,4 +163,16 @@ public void addStyleTag(ClothesStyleTag styleTag) {
161163
public void updateFavorite(Boolean isFavorite) {
162164
this.isFavorite = isFavorite;
163165
}
166+
167+
public void convertToOwned(String productCode, Boolean isVerified) {
168+
if (this.sourceType != SourceType.WISHLIST) {
169+
throw new IllegalArgumentException("미보유 옷만 보유 옷으로 전환할 수 있습니다.");
170+
}
171+
this.sourceType = SourceType.OWNED;
172+
this.externalSource = EXTERNAL_NONE;
173+
this.externalProductId = EXTERNAL_NONE;
174+
this.externalProductUrl = EXTERNAL_NONE;
175+
this.productCode = productCode;
176+
this.isVerified = isVerified;
177+
}
164178
}

src/main/java/com/closetnangam/be/domain/clothes/service/ClothesService.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import com.closetnangam.be.domain.catalog.entity.Style;
44
import com.closetnangam.be.domain.catalog.repository.StyleRepository;
55
import com.closetnangam.be.domain.catalog.service.CategoryCatalogService;
6+
import com.closetnangam.be.domain.clothes.dto.request.ClothesConvertToOwnedRequest;
67
import com.closetnangam.be.domain.clothes.dto.request.ClothesCreateRequest;
78
import com.closetnangam.be.domain.clothes.dto.request.ClothesFavoriteRequest;
89
import com.closetnangam.be.domain.clothes.dto.request.ClothesUpdateRequest;
10+
import com.closetnangam.be.domain.clothes.dto.request.WishlistClothesCreateRequest;
911
import com.closetnangam.be.domain.clothes.dto.response.ClothesResponse;
1012
import com.closetnangam.be.domain.clothes.entity.Clothes;
1113
import com.closetnangam.be.domain.clothes.entity.ClothesStyleTag;
@@ -43,6 +45,18 @@ public List<ClothesResponse> getFavoriteOwnedClothes(Long userId) {
4345
.toList();
4446
}
4547

48+
public List<ClothesResponse> getWishlistClothes(Long userId) {
49+
return clothesRepository.findAllByUserIdAndSourceType(userId, SourceType.WISHLIST).stream()
50+
.map(ClothesResponse::from)
51+
.toList();
52+
}
53+
54+
public List<ClothesResponse> getFavoriteWishlistClothes(Long userId) {
55+
return clothesRepository.findFavoritesByUserIdAndSourceType(userId, SourceType.WISHLIST).stream()
56+
.map(ClothesResponse::from)
57+
.toList();
58+
}
59+
4660
public ClothesResponse getClothes(Long clothesId) {
4761
Clothes clothes = getClothesWithDetails(clothesId);
4862
return ClothesResponse.from(clothes);
@@ -76,6 +90,41 @@ public ClothesResponse createOwnedClothes(Long userId, ClothesCreateRequest requ
7690
return ClothesResponse.from(saved);
7791
}
7892

93+
@Transactional
94+
public ClothesResponse createWishlistClothes(Long userId, WishlistClothesCreateRequest request) {
95+
validateClassification(request.category(), request.itemType(), request.color(), request.styles());
96+
97+
Wardrobe wardrobe = wardrobeService.getOrCreateWardrobe(userId);
98+
99+
Clothes clothes = Clothes.builder()
100+
.wardrobe(wardrobe)
101+
.name(request.name())
102+
.brandName(request.brandName())
103+
.productCode(request.productCode())
104+
.imageUrl(request.imageUrl())
105+
.category(request.category())
106+
.itemType(request.itemType())
107+
.color(request.color())
108+
.sourceType(SourceType.WISHLIST)
109+
.externalSource(request.externalSource())
110+
.externalProductId(request.externalProductId())
111+
.externalProductUrl(request.externalProductUrl())
112+
.isVerified(false)
113+
.isFavorite(false)
114+
.build();
115+
116+
applyStyleTags(clothes, request.styles());
117+
Clothes saved = clothesRepository.save(clothes);
118+
return ClothesResponse.from(saved);
119+
}
120+
121+
@Transactional
122+
public ClothesResponse convertToOwned(Long clothesId, ClothesConvertToOwnedRequest request) {
123+
Clothes clothes = getClothesWithDetails(clothesId);
124+
clothes.convertToOwned(request.productCode(), request.isVerified());
125+
return ClothesResponse.from(clothes);
126+
}
127+
79128
@Transactional
80129
public ClothesResponse updateClothes(Long clothesId, ClothesUpdateRequest request) {
81130
validateClassification(request.category(), request.itemType(), request.color(), request.styles());

0 commit comments

Comments
 (0)