Skip to content

Commit 5263003

Browse files
authored
Merge pull request #21 from prgrms-aibe-devcourse/feat/#18
Feat/#18-보유옷-CRUD
2 parents 6425ef3 + bcc5328 commit 5263003

14 files changed

Lines changed: 698 additions & 9 deletions

File tree

src/main/java/com/closetnangam/be/domain/catalog/repository/StyleRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public interface StyleRepository extends JpaRepository<Style, Long> {
1010

1111
Optional<Style> findByCode(String code);
1212

13+
List<Style> findByCodeIn(List<String> codes);
14+
1315
List<Style> findAllByOrderByCodeAsc();
1416

1517
boolean existsByCode(String code);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ public void validateClothesClassification(String categoryCode, String itemTypeCo
136136
ClothesColor.fromCode(colorCode);
137137
}
138138

139+
public void validateStyleCodes(List<String> styleCodes) {
140+
if (styleCodes == null || styleCodes.isEmpty()) {
141+
throw new IllegalArgumentException("스타일은 1개 이상 선택해야 합니다.");
142+
}
143+
for (String styleCode : styleCodes) {
144+
StyleCode.fromCode(styleCode);
145+
}
146+
}
147+
139148
private java.util.List<CategoryGroupResponse> getCategoryGroups() {
140149
return Arrays.stream(ClothesCategory.values())
141150
.map(category -> new CategoryGroupResponse(
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.closetnangam.be.domain.clothes.controller;
2+
3+
import com.closetnangam.be.domain.clothes.dto.request.ClothesCreateRequest;
4+
import com.closetnangam.be.domain.clothes.dto.request.ClothesFavoriteRequest;
5+
import com.closetnangam.be.domain.clothes.dto.request.ClothesUpdateRequest;
6+
import com.closetnangam.be.domain.clothes.dto.response.ClothesResponse;
7+
import com.closetnangam.be.domain.clothes.service.ClothesService;
8+
import com.closetnangam.be.global.common.response.ApiResponse;
9+
import io.swagger.v3.oas.annotations.Operation;
10+
import io.swagger.v3.oas.annotations.tags.Tag;
11+
import jakarta.validation.Valid;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.http.HttpStatus;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.web.bind.annotation.DeleteMapping;
16+
import org.springframework.web.bind.annotation.GetMapping;
17+
import org.springframework.web.bind.annotation.PatchMapping;
18+
import org.springframework.web.bind.annotation.PathVariable;
19+
import org.springframework.web.bind.annotation.PostMapping;
20+
import org.springframework.web.bind.annotation.RequestBody;
21+
import org.springframework.web.bind.annotation.ResponseStatus;
22+
import org.springframework.web.bind.annotation.RestController;
23+
24+
import java.util.List;
25+
26+
@Tag(name = "Clothes", description = "보유 옷 CRUD API")
27+
@RestController
28+
@RequiredArgsConstructor
29+
public class ClothesController {
30+
31+
private final ClothesService clothesService;
32+
33+
// TODO: JWT 인증 구현 후 @PreAuthorize 또는 SecurityContextHolder로 userId 소유권 검증 추가 필요
34+
@Operation(summary = "보유 옷 목록 조회", description = "사용자 옷장의 보유 옷(OWNED) 목록을 조회합니다.")
35+
@GetMapping("/api/users/{userId}/clothes")
36+
public ResponseEntity<ApiResponse<List<ClothesResponse>>> getOwnedClothes(@PathVariable Long userId) {
37+
return ResponseEntity.ok(ApiResponse.ok(clothesService.getOwnedClothes(userId)));
38+
}
39+
40+
@Operation(summary = "즐겨찾기 옷 목록 조회", description = "즐겨찾기로 표시한 보유 옷 목록을 조회합니다.")
41+
@GetMapping("/api/users/{userId}/clothes/favorites")
42+
public ResponseEntity<ApiResponse<List<ClothesResponse>>> getFavoriteOwnedClothes(@PathVariable Long userId) {
43+
return ResponseEntity.ok(ApiResponse.ok(clothesService.getFavoriteOwnedClothes(userId)));
44+
}
45+
46+
@Operation(summary = "옷 상세 조회", description = "옷 이미지, 이름, 브랜드, 카테고리, 타입, 색상, 스타일 정보를 조회합니다.")
47+
@GetMapping("/api/clothes/{clothesId}")
48+
public ResponseEntity<ApiResponse<ClothesResponse>> getClothes(@PathVariable Long clothesId) {
49+
return ResponseEntity.ok(ApiResponse.ok(clothesService.getClothes(clothesId)));
50+
}
51+
52+
@Operation(summary = "보유 옷 등록", description = "사용자 옷장에 보유 옷을 등록합니다.")
53+
@PostMapping("/api/users/{userId}/clothes")
54+
@ResponseStatus(HttpStatus.CREATED)
55+
public ResponseEntity<ApiResponse<ClothesResponse>> createOwnedClothes(
56+
@PathVariable Long userId,
57+
@Valid @RequestBody ClothesCreateRequest request
58+
) {
59+
return ResponseEntity.status(HttpStatus.CREATED)
60+
.body(ApiResponse.ok(clothesService.createOwnedClothes(userId, request)));
61+
}
62+
63+
@Operation(summary = "옷 즐겨찾기 설정", description = "옷의 즐겨찾기 상태를 등록/해제합니다.")
64+
@PatchMapping("/api/clothes/{clothesId}/favorite")
65+
public ResponseEntity<ApiResponse<ClothesResponse>> updateFavorite(
66+
@PathVariable Long clothesId,
67+
@Valid @RequestBody ClothesFavoriteRequest request
68+
) {
69+
return ResponseEntity.ok(ApiResponse.ok(clothesService.updateFavorite(clothesId, request)));
70+
}
71+
72+
@Operation(summary = "옷 정보 수정", description = "등록된 보유 옷 정보를 수정합니다.")
73+
@PatchMapping("/api/clothes/{clothesId}")
74+
public ResponseEntity<ApiResponse<ClothesResponse>> updateClothes(
75+
@PathVariable Long clothesId,
76+
@Valid @RequestBody ClothesUpdateRequest request
77+
) {
78+
return ResponseEntity.ok(ApiResponse.ok(clothesService.updateClothes(clothesId, request)));
79+
}
80+
81+
@Operation(summary = "옷 삭제", description = "등록된 보유 옷을 삭제합니다. 삭제 확인은 프론트에서 처리합니다.")
82+
@DeleteMapping("/api/clothes/{clothesId}")
83+
@ResponseStatus(HttpStatus.NO_CONTENT)
84+
public ResponseEntity<Void> deleteClothes(@PathVariable Long clothesId) {
85+
clothesService.deleteClothes(clothesId);
86+
return ResponseEntity.noContent().build();
87+
}
88+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.NotNull;
6+
import jakarta.validation.constraints.Size;
7+
8+
import java.util.List;
9+
10+
public record ClothesCreateRequest(
11+
@NotBlank @Size(max = 255) String name,
12+
@NotBlank @Size(max = 100) String brandName,
13+
@NotBlank @Size(max = 100) String productCode,
14+
@NotBlank @Size(max = 500) String imageUrl,
15+
@NotBlank @Size(max = 50) String category,
16+
@NotBlank @Size(max = 50) String itemType,
17+
@NotBlank @Size(max = 50) String color,
18+
@NotEmpty List<@NotBlank String> styles,
19+
@NotNull Boolean isVerified
20+
) {
21+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.closetnangam.be.domain.clothes.dto.request;
2+
3+
import jakarta.validation.constraints.NotNull;
4+
5+
public record ClothesFavoriteRequest(
6+
@NotNull Boolean isFavorite
7+
) {
8+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.NotNull;
6+
import jakarta.validation.constraints.Size;
7+
8+
import java.util.List;
9+
10+
public record ClothesUpdateRequest(
11+
@NotBlank @Size(max = 255) String name,
12+
@NotBlank @Size(max = 100) String brandName,
13+
@NotBlank @Size(max = 100) String productCode,
14+
@NotBlank @Size(max = 500) String imageUrl,
15+
@NotBlank @Size(max = 50) String category,
16+
@NotBlank @Size(max = 50) String itemType,
17+
@NotBlank @Size(max = 50) String color,
18+
@NotEmpty List<@NotBlank String> styles,
19+
@NotNull Boolean isVerified
20+
) {
21+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.closetnangam.be.domain.clothes.dto.response;
2+
3+
import com.closetnangam.be.domain.catalog.enums.ClothesColor;
4+
import com.closetnangam.be.domain.clothes.entity.Clothes;
5+
import com.closetnangam.be.domain.clothes.enums.SourceType;
6+
7+
import java.time.LocalDateTime;
8+
import java.util.List;
9+
10+
public record ClothesResponse(
11+
Long clothesId,
12+
Long wardrobeId,
13+
Long userId,
14+
String name,
15+
String brandName,
16+
String productCode,
17+
String imageUrl,
18+
String category,
19+
String itemType,
20+
String color,
21+
ColorDisplayResponse colorDisplay,
22+
List<StyleTagResponse> styles,
23+
SourceType sourceType,
24+
String externalSource,
25+
String externalProductId,
26+
String externalProductUrl,
27+
Boolean isVerified,
28+
Boolean isFavorite,
29+
LocalDateTime createdAt,
30+
LocalDateTime updatedAt
31+
) {
32+
33+
public static ClothesResponse from(Clothes clothes) {
34+
ClothesColor clothesColor = ClothesColor.fromCode(clothes.getColor());
35+
36+
List<StyleTagResponse> styles = clothes.getStyleTags().stream()
37+
.map(tag -> new StyleTagResponse(
38+
tag.getStyle().getId(),
39+
tag.getStyle().getCode(),
40+
tag.getStyle().getName()
41+
))
42+
.toList();
43+
44+
return new ClothesResponse(
45+
clothes.getId(),
46+
clothes.getWardrobe().getId(),
47+
clothes.getWardrobe().getUser().getId(),
48+
clothes.getName(),
49+
clothes.getBrandName(),
50+
clothes.getProductCode(),
51+
clothes.getImageUrl(),
52+
clothes.getCategory(),
53+
clothes.getItemType(),
54+
clothes.getColor(),
55+
new ColorDisplayResponse(
56+
clothesColor.name(),
57+
clothesColor.getLabel(),
58+
clothesColor.getHex()
59+
),
60+
styles,
61+
clothes.getSourceType(),
62+
clothes.getExternalSource(),
63+
clothes.getExternalProductId(),
64+
clothes.getExternalProductUrl(),
65+
clothes.getIsVerified(),
66+
clothes.getIsFavorite(),
67+
clothes.getCreatedAt(),
68+
clothes.getUpdatedAt()
69+
);
70+
}
71+
72+
public record ColorDisplayResponse(
73+
String code,
74+
String name,
75+
String hex
76+
) {
77+
}
78+
79+
public record StyleTagResponse(
80+
Long styleId,
81+
String code,
82+
String name
83+
) {
84+
}
85+
}

0 commit comments

Comments
 (0)