Skip to content

Commit bbc51a7

Browse files
authored
Merge pull request #14 from NET-ZERO-FitFit/develop
[FEAT] main <- develop (옷 검색 API, 키워드 검색 이력 조회 API 추가)
2 parents f747904 + 2c1c89c commit bbc51a7

24 files changed

Lines changed: 542 additions & 60 deletions

build.gradle

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ plugins {
66

77
group = 'fitfit'
88
version = '0.0.1-SNAPSHOT'
9-
description = 'Demo project for Spring Boot'
109

1110
java {
1211
toolchain {
@@ -64,9 +63,32 @@ dependencies {
6463
runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5') // JSON parsing
6564

6665
// S3
67-
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.559'
66+
implementation('com.amazonaws:aws-java-sdk-s3:1.12.559') {
67+
exclude group: 'commons-logging', module: 'commons-logging'
68+
}
69+
70+
// QueryDSL
71+
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
72+
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
73+
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
74+
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
6875
}
6976

7077
tasks.named('test') {
7178
useJUnitPlatform()
7279
}
80+
81+
// QueryDSL
82+
def querydslDir = "src/main/generated"
83+
84+
sourceSets {
85+
main.java.srcDirs += [ querydslDir ]
86+
}
87+
88+
tasks.withType(JavaCompile) {
89+
options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
90+
}
91+
92+
clean.doLast {
93+
file(querydslDir).deleteDir()
94+
}

src/main/java/fitfit/domain/chat/dto/ChatResponseDTO.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ public class ChatResponseDTO {
1212
@Getter
1313
@Builder
1414
public static class ChatHistoryDTO {
15-
@Schema(description = "채팅 메시지 목록")
15+
@Schema(description = "채팅 내역")
1616
private List<ChatMessageDTO> messages;
1717
}
1818

1919
@Getter
2020
@Builder
2121
public static class ChatMessageDTO {
22-
@Schema(example = "user", description = "메시지 발신자 (user 또는 bot)")
22+
@Schema(description = "송신자", example = "user")
2323
private String sender; // "user" or "bot"
24-
@Schema(example = "안녕하세요")
24+
@Schema(description = "채팅 내용", example = "오늘 날씨 어때?")
2525
private String content;
26-
@Schema(example = "2025-11-28T12:00:00")
26+
@Schema(description = "생성 시각", example = "2024-07-26T10:00:00")
2727
private LocalDateTime createdAt;
2828
}
2929
}

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package fitfit.domain.clothes.controller;
22

3+
import fitfit.domain.clothes.converter.ClothesConverter;
34
import fitfit.domain.clothes.dto.ClothesRequestDTO;
45
import fitfit.domain.clothes.dto.ClothesResponseDTO;
6+
import fitfit.domain.clothes.entity.Clothes;
57
import fitfit.domain.clothes.service.ClothesCommandService;
8+
import fitfit.domain.clothes.service.ClothesQueryService;
9+
import fitfit.domain.search.dto.SearchResponseDTO;
10+
import fitfit.domain.search.service.SearchCommandService;
11+
import fitfit.domain.search.service.SearchQueryService;
612
import fitfit.global.apiPayload.ApiResponse;
713
import io.swagger.v3.oas.annotations.Operation;
814
import io.swagger.v3.oas.annotations.media.Content;
@@ -11,7 +17,7 @@
1117
import io.swagger.v3.oas.annotations.tags.Tag;
1218
import jakarta.validation.Valid;
1319
import lombok.RequiredArgsConstructor;
14-
import org.springframework.http.MediaType;
20+
import org.springframework.data.domain.Page;
1521
import org.springframework.web.bind.annotation.*;
1622

1723
import java.io.IOException;
@@ -23,6 +29,9 @@
2329
public class ClothesRestController {
2430

2531
private final ClothesCommandService clothesCommandService;
32+
private final ClothesQueryService clothesQueryService;
33+
private final SearchCommandService searchCommandService;
34+
private final SearchQueryService searchQueryService;
2635

2736
@PostMapping("/post")
2837
@Operation(summary = "판매 옷 등록 API", description = "자신이 판매할 옷의 정보를 등록하는 API입니다. 이미지는 Base64 인코딩된 문자열로 보내주세요.")
@@ -41,4 +50,62 @@ public ApiResponse<ClothesResponseDTO.CreateClothesResponse> registerClothes(
4150
ClothesResponseDTO.CreateClothesResponse response = clothesCommandService.createClothes(authorization, requestDto);
4251
return ApiResponse.onSuccess(response);
4352
}
53+
54+
@GetMapping("/search")
55+
@Operation(summary = "판매 옷 일반 검색 API", description = """
56+
키워드 검색을 통해 판매 옷 정보를 가져오는 API입니다. (존재하지 않는 키워드인 경우 빈 리스트로 응답)
57+
**페이지네이션(Pagination) 설명:**
58+
- `page` 파라미터는 요청할 페이지 번호입니다 (0부터 시작).
59+
- `size` 파라미터는 한 페이지에 가져올 아이템 개수입니다 (기본값은 백엔드에서 설정).
60+
- 응답은 `ClothesPageDTO` 형식으로 반환됩니다.
61+
- `clothesList`: 현재 페이지에 해당하는 옷 데이터 목록입니다.
62+
- `listSize`: 현재 페이지의 아이템 수입니다.
63+
- `totalPage`: 전체 페이지 수입니다.
64+
- `totalElements`: 전체 아이템 수입니다.
65+
- `isFirst`: 현재 페이지가 첫 페이지인지 여부입니다. (true/false)
66+
- `isLast`: 현재 페이지가 마지막 페이지인지 여부입니다. (true/false)
67+
""")
68+
@ApiResponses({
69+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK, 성공"),
70+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Bad Request, 잘못된 요청 형식", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
71+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "Unauthorized, 유효하지 않은 토큰", content = @Content(schema = @Schema(implementation = ApiResponse.class)))
72+
})
73+
public ApiResponse<ClothesResponseDTO.ClothesPageDTO> searchClothes(
74+
@RequestHeader(value = "Authorization") String authorization,
75+
@RequestParam(name = "keyword") String keyword, // 검색할 키워드를 요청 파라미터로 받음
76+
@RequestParam(name = "page") Integer page // 페이지 번호를 요청 파라미터로 받음
77+
) {
78+
searchCommandService.createSearchRecord(keyword, authorization);
79+
// 키워드와 페이지 번호로 옷을 검색
80+
Page<Clothes> clothesPage = clothesQueryService.findClothesByKeyword(keyword, page);
81+
// 검색 결과를 DTO로 변환하여 성공 응답 반환
82+
return ApiResponse.onSuccess(ClothesConverter.toClothesPageDTO(clothesPage));
83+
}
84+
85+
@GetMapping("/search-record")
86+
@Operation(summary = "최근 검색어 조회 API", description = """
87+
최근 검색어 목록을 최신순으로 조회하는 API입니다.
88+
**페이지네이션(Pagination) 설명:**
89+
- `page` 파라미터는 요청할 페이지 번호입니다 (0부터 시작).
90+
- `size` 파라미터는 한 페이지에 가져올 아이템 개수입니다 (기본값은 백엔드에서 설정).
91+
- 응답은 `SearchRecordPageDTO` 형식으로 반환됩니다.
92+
- `searchRecordList`: 현재 페이지에 해당하는 검색 기록 데이터 목록입니다.
93+
- `listSize`: 현재 페이지의 아이템 수입니다.
94+
- `totalPage`: 전체 페이지 수입니다.
95+
- `totalElements`: 전체 아이템 수입니다.
96+
- `isFirst`: 현재 페이지가 첫 페이지인지 여부입니다. (true/false)
97+
- `isLast`: 현재 페이지가 마지막 페이지인지 여부입니다. (true/false)
98+
""")
99+
@ApiResponses({
100+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK, 성공"),
101+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "Unauthorized, 유효하지 않은 토큰", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
102+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Not Found, 존재하지 않는 회원", content = @Content(schema = @Schema(implementation = ApiResponse.class)))
103+
})
104+
public ApiResponse<SearchResponseDTO.SearchRecordPageDTO> getSearchRecord(
105+
@RequestHeader(value = "Authorization") String authorization,
106+
@RequestParam(name = "page") Integer page
107+
) {
108+
SearchResponseDTO.SearchRecordPageDTO response = searchQueryService.getSearchRecord(authorization, page);
109+
return ApiResponse.onSuccess(response);
110+
}
44111
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
import fitfit.domain.clothes.entity.Clothes;
77
import fitfit.domain.member.entity.Member;
88
import fitfit.global.enums.Style;
9+
import org.springframework.data.domain.Page;
910

1011
import java.time.LocalDateTime;
12+
import java.util.List;
13+
import java.util.stream.Collectors;
1114

1215
public class ClothesConverter {
1316

@@ -45,4 +48,33 @@ public static ClothesResponseDTO.CreateClothesResponse toCreateClothesResponse(C
4548
.createdAt(clothes.getCreatedAt())
4649
.build();
4750
}
51+
52+
// Clothes 엔티티를 ClothesPreviewDTO로 변환
53+
public static ClothesResponseDTO.ClothesPreviewDTO toClothesPreviewDTO(Clothes clothes) {
54+
return ClothesResponseDTO.ClothesPreviewDTO.builder()
55+
.clothesId(clothes.getId())
56+
.title(clothes.getTitle())
57+
.style(clothes.getStyle().toString())
58+
.price(clothes.getPrice())
59+
.fittingImage(clothes.getFittingImage())
60+
.createdAt(clothes.getCreatedAt())
61+
.build();
62+
}
63+
64+
// Page<Clothes>를 ClothesPageDTO로 변환
65+
public static ClothesResponseDTO.ClothesPageDTO toClothesPageDTO(Page<Clothes> clothesPage) {
66+
// 각 옷을 ClothesPreviewDTO로 변환
67+
List<ClothesResponseDTO.ClothesPreviewDTO> clothesList = clothesPage.stream()
68+
.map(ClothesConverter::toClothesPreviewDTO).collect(Collectors.toList());
69+
70+
// 최종 페이지 DTO 생성
71+
return ClothesResponseDTO.ClothesPageDTO.builder()
72+
.clothesList(clothesList)
73+
.listSize(clothesList.size())
74+
.totalPage(clothesPage.getTotalPages())
75+
.totalElements(clothesPage.getTotalElements())
76+
.isFirst(clothesPage.isFirst())
77+
.isLast(clothesPage.isLast())
78+
.build();
79+
}
4880
}

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

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,64 +16,65 @@ public class ClothesRequestDTO {
1616
public static class CreateClothesRequest {
1717

1818
@NotBlank(message = "제목은 필수입니다.")
19-
@Schema(example = "판매글 제목")
19+
@Schema(description = "제목", example = "데일리 룩")
2020
private String title;
2121

2222
@NotNull(message = "가격은 필수입니다.")
23-
@Schema(example = "10000")
24-
private Long price;
23+
@Schema(description = "가격", example = "30000")
24+
private Integer price;
2525

2626
@NotNull(message = "카테고리 ID는 필수입니다.")
27-
@Schema(example = "1")
27+
@Schema(description = "카테고리 ID", example = "1")
2828
private Long categoryId;
2929

3030
@NotBlank(message = "스타일은 필수입니다.")
31-
@Schema(example = "VINTAGE")
31+
@Schema(description = "스타일", example = "BASIC")
3232
private String style;
3333

34-
@Schema(example = "판매글 내용")
34+
@Schema(description = "코멘트", example = "이 옷은 매우 편안합니다.")
3535
private String comment;
3636

37-
@Schema(example = "서울시 강남구")
37+
@NotBlank(message = "주소는 필수입니다.")
38+
@Schema(description = "주소", example = "서울시 강남구")
3839
private String address;
3940

40-
@Schema(example = "37.4979")
41+
@Schema(description = "위도", example = "37.123456")
4142
private Double lat;
42-
@Schema(example = "127.0276")
43+
@Schema(description = "경도", example = "127.123456")
4344
private Double lng;
44-
@Schema(example = "true")
45+
@Schema(description = "판매 동의 여부", example = "true")
4546
private Boolean saleAgreed;
46-
@Schema(example = "true")
47+
@Schema(description = "만남 동의 여부", example = "false")
4748
private Boolean meetupAgreed;
48-
@Schema(example = "false")
49+
@Schema(description = "제안 동의 여부", example = "true")
4950
private Boolean offerAgreed;
5051

5152
// 사이즈 정보
52-
@Schema(example = "100")
53+
@Schema(description = "총장", example = "100")
5354
private String totalLength;
54-
@Schema(example = "50")
55+
@Schema(description = "가슴 단면", example = "50")
5556
private String chestWidth;
56-
@Schema(example = "45")
57+
@Schema(description = "어깨 너비", example = "45")
5758
private String shoulderWidth;
58-
@Schema(example = "270")
59+
@Schema(description = "발 사이즈", example = "270")
5960
private String footSize;
60-
@Schema(example = "40")
61+
@Schema(description = "허리 단면", example = "40")
6162
private String waistMeasurement;
62-
@Schema(example = "30")
63+
@Schema(description = "허벅지 단면", example = "30")
6364
private String thighMeasurement;
6465

6566
// 판매 정보
66-
@Schema(example = "2025-12-31")
67+
@Schema(description = "판매 기간", example = "2024-12-31")
6768
private LocalDate salesPeriod;
68-
@Schema(example = "10")
69+
@Schema(description = "할인율", example = "10")
6970
private Long discountRate;
7071

7172
// Base64 인코딩된 이미지 문자열을 받습니다.
7273
@NotBlank(message = "피팅 이미지는 필수입니다.")
73-
@Schema(example = "data:image/jpeg;base64,...")
74+
@Schema(description = "피팅 이미지", example = "data:image/jpeg;base64,...")
7475
private String fittingImage; // "data:image/jpeg;base64,..."
7576

76-
@Schema(example = "[\"data:image/jpeg;base64,...\"]")
77+
@Schema(description = "전시 이미지 목록", example = "[\"https://example.com/image1.jpg\", \"https://example.com/image2.jpg\"]")
7778
private List<String> displayImages;
7879
}
7980
}

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

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import lombok.NoArgsConstructor;
88

99
import java.time.LocalDateTime;
10+
import java.util.List;
1011

1112
public class ClothesResponseDTO {
1213

@@ -15,9 +16,49 @@ public class ClothesResponseDTO {
1516
@NoArgsConstructor
1617
@AllArgsConstructor
1718
public static class CreateClothesResponse {
18-
@Schema(example = "1")
19+
@Schema(description = "옷 ID", example = "1")
1920
private Long clothesId;
20-
@Schema(example = "2025-11-28T12:00:00")
21+
@Schema(description = "생성 시각", example = "2024-07-26T10:00:00")
2122
private LocalDateTime createdAt;
2223
}
24+
25+
// 옷 검색 결과 미리보기 DTO
26+
@Builder
27+
@Getter
28+
@NoArgsConstructor
29+
@AllArgsConstructor
30+
public static class ClothesPreviewDTO {
31+
@Schema(description = "옷 ID", example = "1")
32+
private Long clothesId;
33+
@Schema(description = "제목", example = "데일리 룩")
34+
private String title;
35+
@Schema(description = "스타일", example = "BASIC")
36+
private String style;
37+
@Schema(description = "가격", example = "30000")
38+
private Integer price;
39+
@Schema(description = "피팅 이미지", example = "https://example.com/fitting.jpg")
40+
private String fittingImage;
41+
@Schema(description = "생성 시각", example = "2024-07-26T10:00:00")
42+
private LocalDateTime createdAt;
43+
}
44+
45+
// 옷 검색 결과 페이지 DTO
46+
@Builder
47+
@Getter
48+
@NoArgsConstructor
49+
@AllArgsConstructor
50+
public static class ClothesPageDTO {
51+
@Schema(description = "옷 목록")
52+
private List<ClothesPreviewDTO> clothesList; // 옷 목록
53+
@Schema(description = "현재 페이지의 아이템 수", example = "10")
54+
private Integer listSize; // 현재 페이지의 아이템 수
55+
@Schema(description = "전체 페이지 수", example = "5")
56+
private Integer totalPage; // 전체 페이지 수
57+
@Schema(description = "전체 아이템 수", example = "50")
58+
private Long totalElements; // 전체 아이템 수
59+
@Schema(description = "첫 페이지 여부", example = "true")
60+
private Boolean isFirst; // 첫 페이지 여부
61+
@Schema(description = "마지막 페이지 여부", example = "false")
62+
private Boolean isLast; // 마지막 페이지 여부
63+
}
2364
}

src/main/java/fitfit/domain/clothes/entity/Clothes.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class Clothes extends BaseEntity {
3737
private String title; // 제목
3838

3939
@Column(nullable = false)
40-
private Long price; // 가격
40+
private Integer price; // 가격
4141

4242
// S3 'fitting/' 폴더에 저장된 URL
4343
@Column(name = "fitting_image", nullable = false)
@@ -61,7 +61,7 @@ public class Clothes extends BaseEntity {
6161
@Column(name = "meetup_agreed")
6262
private Boolean meetupAgreed; // 직거래 동의 여부
6363

64-
@Column(length = 25, nullable = false)
64+
@Column(length = 25)
6565
private String address; // 지역명
6666

6767
@Column(name = "is_sold")

src/main/java/fitfit/domain/clothes/repository/ClothesRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
import fitfit.domain.clothes.entity.Clothes;
44
import org.springframework.data.jpa.repository.JpaRepository;
55

6-
public interface ClothesRepository extends JpaRepository<Clothes, Long> {
6+
public interface ClothesRepository extends JpaRepository<Clothes, Long>, ClothesRepositoryCustom {
77
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fitfit.domain.clothes.repository;
2+
3+
import fitfit.domain.clothes.entity.Clothes;
4+
import org.springframework.data.domain.Page;
5+
import org.springframework.data.domain.Pageable;
6+
7+
public interface ClothesRepositoryCustom {
8+
9+
Page<Clothes> findByKeyword(String keyword, Pageable pageable);
10+
}

0 commit comments

Comments
 (0)