Skip to content

Commit 45e26d0

Browse files
authored
Merge pull request #28 from NET-ZERO-FitFit/develop
main <- develop
2 parents 4e5c490 + bf4198a commit 45e26d0

18 files changed

Lines changed: 690 additions & 28 deletions

src/main/java/fitfit/domain/clothes/controller/ClothesRestController.java

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
import fitfit.global.apiPayload.ApiResponse;
1313
import fitfit.global.apiPayload.code.status.SuccessStatus;
1414
import io.swagger.v3.oas.annotations.Operation;
15+
import io.swagger.v3.oas.annotations.Parameter;
1516
import io.swagger.v3.oas.annotations.media.Content;
1617
import io.swagger.v3.oas.annotations.media.Schema;
1718
import io.swagger.v3.oas.annotations.responses.ApiResponses;
1819
import io.swagger.v3.oas.annotations.tags.Tag;
1920
import jakarta.validation.Valid;
2021
import lombok.RequiredArgsConstructor;
2122
import org.springframework.data.domain.Page;
23+
import org.springframework.security.core.parameters.P;
2224
import org.springframework.web.bind.annotation.*;
2325

2426
import java.io.IOException;
@@ -84,6 +86,30 @@ public ApiResponse<ClothesResponseDTO.UpdateResponse> updateClothes(
8486
return ApiResponse.onSuccess(response);
8587
}
8688

89+
@GetMapping("/filter")
90+
@Operation(summary = "옷 목록 필터링 조회 API", description = """
91+
카테고리, 스타일, 가격대, 위치를 기준으로 옷 목록을 동적 필터링여 조회합니다.
92+
- `page`는 0부터 시작합니다.
93+
- `FilterClothesRequest` DTO는 쿼리 파라미터로 전달됩니다. (주소창에 직접 입력하거나, JS에서 URLSearchParams로 전송)
94+
""")
95+
@ApiResponses({
96+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK, 성공"),
97+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Bad Request, 잘못된 요청 형식", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
98+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "Unauthorized, 유효하지 않은 토큰", content = @Content(schema = @Schema(implementation = ApiResponse.class)))
99+
})
100+
public ApiResponse<ClothesResponseDTO.ClothesPageDTO> getFilteredClothes(
101+
@RequestHeader(value = "Authorization") String authorization,
102+
@Valid @ModelAttribute ClothesRequestDTO.FilterClothesRequest request,
103+
@RequestParam(name = "page") @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") Integer page
104+
) {
105+
// 1. 필터링된 옷 목록 조회
106+
Page<Clothes> clothesPage = clothesQueryService.getFilteredClothes(request, page);
107+
108+
// 2. 응답 DTO로 변환
109+
return ApiResponse.onSuccess(ClothesConverter.toClothesPageDTO(clothesPage));
110+
}
111+
112+
87113
@GetMapping("/search")
88114
@Operation(summary = "판매 옷 일반 검색 API", description = """
89115
키워드 검색을 통해 판매 옷 정보를 가져오는 API입니다. (존재하지 않는 키워드인 경우 빈 리스트로 응답)
@@ -97,7 +123,7 @@ public ApiResponse<ClothesResponseDTO.UpdateResponse> updateClothes(
97123
- `totalElements`: 전체 아이템 수입니다.
98124
- `isFirst`: 현재 페이지가 첫 페이지인지 여부입니다. (true/false)
99125
- `isLast`: 현재 페이지가 마지막 페이지인지 여부입니다. (true/false)
100-
""")
126+
""", tags = {"Search"})
101127
@ApiResponses({
102128
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK, 성공"),
103129
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Bad Request, 잘못된 요청 형식", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@@ -128,7 +154,7 @@ public ApiResponse<ClothesResponseDTO.ClothesPageDTO> searchClothes(
128154
- `totalElements`: 전체 아이템 수입니다.
129155
- `isFirst`: 현재 페이지가 첫 페이지인지 여부입니다. (true/false)
130156
- `isLast`: 현재 페이지가 마지막 페이지인지 여부입니다. (true/false)
131-
""")
157+
""", tags = {"Search"})
132158
@ApiResponses({
133159
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK, 성공"),
134160
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "Unauthorized, 유효하지 않은 토큰", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@@ -168,4 +194,69 @@ public ApiResponse<List<ClothesResponseDTO.MapMarkerDTO>> getMapMarkers() {
168194
List<ClothesResponseDTO.MapMarkerDTO> result = clothesQueryService.getMapMarkers();
169195
return ApiResponse.of(SuccessStatus._OK, result);
170196
}
197+
198+
@GetMapping("/{clothesId}")
199+
@Operation(
200+
summary = "옷 상세 페이지 조회 API",
201+
description = """
202+
상품 ID로 상세 정보를 조회합니다.
203+
- **기능:** 조회수 1 증가, 상품 정보, 판매자 정보, 이미지 리스트 반환
204+
- **상태값:** `status` (SELLING, MATCHED, SOLD_OUT)에 따라 버튼 활성화 여부를 결정하세요.
205+
"""
206+
)
207+
@ApiResponses({
208+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공 (상세 정보 반환)", content = @Content(schema = @Schema(implementation = ClothesResponseDTO.ClothesDetailDTO.class))),
209+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "존재하지 않는 상품 (CLOTHES4004)", content = @Content(schema = @Schema(implementation = ApiResponse.class)))
210+
})
211+
public ApiResponse<ClothesResponseDTO.ClothesDetailDTO> getClothesDetail(
212+
@RequestHeader(value = "Authorization") String authorization,
213+
@Parameter(description = "조회할 상품 ID") @PathVariable Long clothesId
214+
) {
215+
216+
ClothesResponseDTO.ClothesDetailDTO result = clothesQueryService.getClothesDetail(clothesId, authorization);
217+
return ApiResponse.of(SuccessStatus._OK, result);
218+
}
219+
220+
// 옷장 넣기/빼기 API (토글)
221+
@PostMapping("/{clothesId}/wear-room")
222+
@Operation(summary = "옷장 넣기/빼기 (토글)", description = "상품을 내 옷장에 추가하거나 삭제합니다. (이미 있으면 삭제, 없으면 추가)" +
223+
"is added = true로 오면 옷장에 추가된것임 false로 오면 옷장에서 빠진거")
224+
@ApiResponses({
225+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"),
226+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "존재하지 않는 상품/회원")
227+
})
228+
public ApiResponse<ClothesResponseDTO.ToggleWearRoomDTO> toggleCloset(
229+
@RequestHeader(value = "Authorization") String authorization,
230+
@Parameter(description = "상품 ID") @PathVariable Long clothesId
231+
){
232+
ClothesResponseDTO.ToggleWearRoomDTO result = clothesCommandService.toggleWearRoom(authorization,clothesId);
233+
return ApiResponse.of(SuccessStatus._OK, result);
234+
}
235+
236+
@GetMapping("/wear-room")
237+
@Operation(
238+
summary = "내 옷장(찜 목록) 조회 API",
239+
description = """
240+
내가 옷장에 담은(찜한) 상품 목록을 **담은 순서의 역순(최신순)**으로 조회합니다.
241+
"""
242+
)
243+
@ApiResponses({
244+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
245+
responseCode = "200",
246+
description = "성공 (옷장 리스트 반환)",
247+
content = @Content(schema = @Schema(implementation = ClothesResponseDTO.WearRoomListDTO.class))
248+
),
249+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
250+
responseCode = "401",
251+
description = "인증 실패 (로그인 필요)",
252+
content = @Content(schema = @Schema(implementation = ApiResponse.class))
253+
)
254+
})
255+
public ApiResponse<ClothesResponseDTO.WearRoomListDTO> getMyWearRoom(
256+
@RequestHeader(value = "Authorization") String authorization
257+
) {
258+
ClothesResponseDTO.WearRoomListDTO result = clothesQueryService.getMyWearRoomList(authorization);
259+
260+
return ApiResponse.of(SuccessStatus._OK, result);
261+
}
171262
}

src/main/java/fitfit/domain/clothes/converter/ClothesConverter.java

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import fitfit.domain.clothes.dto.ClothesRequestDTO;
55
import fitfit.domain.clothes.dto.ClothesResponseDTO;
66
import fitfit.domain.clothes.entity.Clothes;
7+
import fitfit.domain.clothes.entity.ClothesImage;
8+
import fitfit.domain.clothes.mapping.WearRoom;
79
import fitfit.domain.member.entity.Member;
810
import fitfit.global.enums.Style;
911
import org.springframework.data.domain.Page;
@@ -54,12 +56,9 @@ public static ClothesResponseDTO.CreateClothesResponse toCreateClothesResponse(C
5456
// Clothes 엔티티를 ClothesPreviewDTO로 변환
5557
public static ClothesResponseDTO.ClothesPreviewDTO toClothesPreviewDTO(Clothes clothes) {
5658
return ClothesResponseDTO.ClothesPreviewDTO.builder()
57-
.clothesId(clothes.getId())
59+
.nickname(clothes.getSeller().getNickname())
5860
.title(clothes.getTitle())
59-
.style(clothes.getStyle().toString())
6061
.price(clothes.getPrice())
61-
.createdAt(clothes.getCreatedAt())
62-
.isMatched(clothes.getIsMatched())
6362
.build();
6463
}
6564

@@ -115,4 +114,77 @@ public static List<ClothesResponseDTO.MapMarkerDTO> toMapMarkerDTOList(List<Clot
115114
.map(ClothesConverter::toMapMarkerDTO)
116115
.collect(Collectors.toList());
117116
}
117+
118+
public static ClothesResponseDTO.ClothesDetailDTO toClothesDetailDTO(Clothes clothes,Boolean isInWearRoom) {
119+
Member seller = clothes.getSeller();
120+
121+
//상태 계산
122+
String status = "SELLING";
123+
if (Boolean.TRUE.equals(clothes.getIsSold())) status = "SOLD_OUT";
124+
else if (Boolean.TRUE.equals(clothes.getIsMatched())) status = "MATCHED";
125+
126+
List<String> imageUrls = clothes.getImages().stream()
127+
.map(ClothesImage::getImageUrl)
128+
.toList();
129+
130+
return ClothesResponseDTO.ClothesDetailDTO.builder()
131+
.clothesId(clothes.getId())
132+
.createdAt(clothes.getCreatedAt())
133+
.title(clothes.getTitle())
134+
.price(clothes.getPrice().longValue())
135+
.status(status)
136+
.seeCount(clothes.getSeeCount())
137+
.isInWearRoom(isInWearRoom)
138+
139+
//판매자
140+
.sellerId(seller.getId())
141+
.sellerNickname(seller.getNickname())
142+
.sellerProfileUrl(seller.getProfileImgUrl())
143+
.sellerCleanIndex(seller.getCleanIndex() != null ? seller.getCleanIndex() : 50)
144+
// 이미지
145+
.images(imageUrls)
146+
// 상세
147+
.comment(clothes.getComment())
148+
.style(clothes.getStyle() != null ? clothes.getStyle().toString() : null)
149+
.categoryName(clothes.getCategory().getName())
150+
.address(clothes.getAddress())
151+
.meetupAgreed(clothes.getMeetupAgreed())
152+
.offerAgreed(clothes.getOfferAgreed())
153+
// 사이즈
154+
.totalLength(clothes.getTotalLength())
155+
.chestWidth(clothes.getChestWidth())
156+
.shoulderWidth(clothes.getShoulderWidth())
157+
.footSize(clothes.getFootSize())
158+
.waistMeasurement(clothes.getWaistMeasurement())
159+
.thighMeasurement(clothes.getThighMeasurement())
160+
.build();
161+
162+
}
163+
164+
public static ClothesResponseDTO.ToggleWearRoomDTO toToggleWearRoomDTO(Boolean isAdded) {
165+
return ClothesResponseDTO.ToggleWearRoomDTO.builder()
166+
.isAdded(isAdded).build();
167+
}
168+
169+
// [옷장 목록 조회용] WearRoom 엔티티 -> ClosetItemDTO 변환
170+
public static ClothesResponseDTO.WearRoomItemDTO toWearRoomItemDTO(WearRoom wearRoom) {
171+
Clothes clothes = wearRoom.getClothes();
172+
Member seller = clothes.getSeller();
173+
174+
// 1. 대표 이미지 추출 (0번째 사진)
175+
String mainImageUrl = null;
176+
if (clothes.getImages() != null && !clothes.getImages().isEmpty()) {
177+
mainImageUrl = clothes.getImages().get(0).getImageUrl();
178+
}
179+
180+
// 2. DTO 생성
181+
return ClothesResponseDTO.WearRoomItemDTO.builder()
182+
.clothesId(clothes.getId())
183+
.imageUrl(mainImageUrl) // 대표 이미지
184+
.sellerNickname(seller.getNickname())
185+
.title(clothes.getTitle())
186+
.price(clothes.getPrice().longValue()) // Long 변환
187+
.createdAt(wearRoom.getCreatedAt()) // 찜한 날짜
188+
.build();
189+
}
118190
}

src/main/java/fitfit/domain/clothes/dto/ClothesRequestDTO.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,24 @@ public static class DeleteRequest {
139139
@Schema(description = "옷 ID", example = "1")
140140
private Long clothesId;
141141
}
142+
143+
@Getter
144+
@NoArgsConstructor
145+
public static class FilterClothesRequest {
146+
//카테고리
147+
@Schema(description = "카테고리 ID (다중 선택 가능)", example = "1, 2, 5")
148+
private List<Long> categoryIds;
149+
150+
//스타일
151+
@Schema(description = "스타일 (다중 선택 가능)", example = "캐주얼, 스트릿")
152+
private List<String> styles;
153+
154+
//가격
155+
@Schema (description = "가격대 코드 (다중 선택 가능)", example = "RANGE_10_20, RANGE_30_40")
156+
private List<String> priceRangeCodes;
157+
158+
// 주소
159+
@Schema(description = "주소 (다중 선택 가능)", example = "서울시 강남구, 서울시 마포구")
160+
private List<String> addresses;
161+
}
142162
}

0 commit comments

Comments
 (0)