Skip to content

Commit fe04266

Browse files
authored
Merge pull request #25 from NET-ZERO-FitFit/develop
main <- develop (판매글 수정, 지도, 유저 채팅 API 추가)
2 parents 0378aaa + 5bc0f4a commit fe04266

35 files changed

Lines changed: 1178 additions & 19 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ build/
77
!**/src/test/**/build/
88
src/main/generated/
99

10+
src/main/resources/application.yml
11+
1012
### STS ###
1113
.apt_generated
1214
.classpath

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ dependencies {
7272
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
7373
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
7474
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
75+
76+
//웹소켓
77+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
7578
}
7679

7780
tasks.named('test') {

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

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import fitfit.domain.search.service.SearchCommandService;
1111
import fitfit.domain.search.service.SearchQueryService;
1212
import fitfit.global.apiPayload.ApiResponse;
13+
import fitfit.global.apiPayload.code.status.SuccessStatus;
1314
import io.swagger.v3.oas.annotations.Operation;
1415
import io.swagger.v3.oas.annotations.media.Content;
1516
import io.swagger.v3.oas.annotations.media.Schema;
@@ -21,6 +22,7 @@
2122
import org.springframework.web.bind.annotation.*;
2223

2324
import java.io.IOException;
25+
import java.util.List;
2426

2527
@RestController
2628
@RequiredArgsConstructor
@@ -51,7 +53,7 @@ public ApiResponse<ClothesResponseDTO.CreateClothesResponse> registerClothes(
5153
return ApiResponse.onSuccess(response);
5254
}
5355

54-
@DeleteMapping("/{clothesId}")
56+
@DeleteMapping("/delete")
5557
@Operation(summary = "판매 옷 삭제 API", description = "자신이 등록한 판매 옷을 삭제하는 API입니다.")
5658
@ApiResponses({
5759
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK, 성공"),
@@ -61,11 +63,27 @@ public ApiResponse<ClothesResponseDTO.CreateClothesResponse> registerClothes(
6163
})
6264
public ApiResponse<String> deleteClothes(
6365
@RequestHeader(value = "Authorization") String authorization,
64-
@PathVariable(name = "clothesId") Long clothesId) {
65-
clothesCommandService.deleteClothes(authorization, clothesId);
66+
@Valid @RequestBody ClothesRequestDTO.DeleteRequest request) {
67+
clothesCommandService.deleteClothes(authorization, request);
6668
return ApiResponse.onSuccess("판매 옷 삭제에 성공했습니다.");
6769
}
6870

71+
@PatchMapping("/update")
72+
@Operation(summary = "판매글 수정 API", description = "자신이 작성한 판매글의 제목, 내용, 가격 등을 선택 수정합니다.")
73+
@ApiResponses({
74+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK, 성공"),
75+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "유효성 검사 실패(잘못된 요청 형식)", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
76+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "수정 권한 없음",content = @Content(schema = @Schema(implementation = ApiResponse.class))),
77+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "판매글을 찾을 수 없음",content = @Content(schema = @Schema(implementation = ApiResponse.class))),
78+
})
79+
public ApiResponse<ClothesResponseDTO.UpdateResponse> updateClothes(
80+
@RequestHeader(value = "Authorization") String authorization,
81+
@Valid @RequestBody ClothesRequestDTO.UpdateRequest request) {
82+
83+
ClothesResponseDTO.UpdateResponse response = clothesCommandService.updateClothes(authorization, request);
84+
return ApiResponse.onSuccess(response);
85+
}
86+
6987
@GetMapping("/search")
7088
@Operation(summary = "판매 옷 일반 검색 API", description = """
7189
키워드 검색을 통해 판매 옷 정보를 가져오는 API입니다. (존재하지 않는 키워드인 경우 빈 리스트로 응답)
@@ -123,4 +141,31 @@ public ApiResponse<SearchResponseDTO.SearchRecordPageDTO> getSearchRecord(
123141
SearchResponseDTO.SearchRecordPageDTO response = searchQueryService.getSearchRecord(authorization, page);
124142
return ApiResponse.onSuccess(response);
125143
}
144+
145+
@GetMapping("/map")
146+
@Operation(
147+
summary = "지도 마커(핀) 전체 조회",
148+
description = """
149+
지도에 표시할 **판매 중인 모든 상품**의 위치와 썸네일 정보를 조회합니다.
150+
151+
- **반환 데이터:** 위도(lat), 경도(lng), 상품ID, 썸네일URL, 가격, 주소
152+
- **사용 시점:** 지도 탭에 처음 들어갔을 때 호출하여 마커를 찍습니다.
153+
- **참고:** 판매 완료된 상품은 조회되지 않습니다.
154+
"""
155+
)
156+
@ApiResponses({
157+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
158+
responseCode = "200",
159+
description = "성공 (마커 리스트 반환)",
160+
content = @Content(schema = @Schema(implementation = ClothesResponseDTO.MapMarkerDTO.class))
161+
),
162+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
163+
responseCode = "500",
164+
description = "서버 에러, 관리자에게 문의 바랍니다. (COMMON500)"
165+
)
166+
})
167+
public ApiResponse<List<ClothesResponseDTO.MapMarkerDTO>> getMapMarkers() {
168+
List<ClothesResponseDTO.MapMarkerDTO> result = clothesQueryService.getMapMarkers();
169+
return ApiResponse.of(SuccessStatus._OK, result);
170+
}
126171
}

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import fitfit.global.enums.Style;
99
import org.springframework.data.domain.Page;
1010

11-
import java.time.LocalDateTime;
1211
import java.util.List;
1312
import java.util.stream.Collectors;
1413

@@ -77,4 +76,40 @@ public static ClothesResponseDTO.ClothesPageDTO toClothesPageDTO(Page<Clothes> c
7776
.isLast(clothesPage.isLast())
7877
.build();
7978
}
79+
80+
//판매글 수정 응답을 위한 컨버터 메서드 추가
81+
public static ClothesResponseDTO.UpdateResponse toUpdateResponse(Clothes clothes) {
82+
return ClothesResponseDTO.UpdateResponse.builder()
83+
.title(clothes.getTitle())
84+
.price(clothes.getPrice())
85+
.comment(clothes.getComment())
86+
.address(clothes.getAddress())
87+
.totalLength(clothes.getTotalLength())
88+
.chestWidth(clothes.getChestWidth())
89+
.shoulderWidth(clothes.getShoulderWidth())
90+
.footSize(clothes.getFootSize())
91+
.waistMeasurement(clothes.getWaistMeasurement())
92+
.thighMeasurement(clothes.getThighMeasurement())
93+
.updatedAt(clothes.getUpdatedAt())
94+
.build();
95+
}
96+
97+
public static ClothesResponseDTO.MapMarkerDTO toMapMarkerDTO(Clothes clothes) {
98+
String mainImageUrl = clothes.getImages().isEmpty() ? null : clothes.getImages().get(0).getImageUrl();
99+
return new ClothesResponseDTO.MapMarkerDTO(
100+
clothes.getId(),
101+
clothes.getLat(),
102+
clothes.getLng(),
103+
mainImageUrl,
104+
clothes.getTitle(),
105+
clothes.getPrice(),
106+
clothes.getAddress()
107+
);
108+
}
109+
110+
public static List<ClothesResponseDTO.MapMarkerDTO> toMapMarkerDTOList(List<Clothes> clothesList) {
111+
return clothesList.stream()
112+
.map(ClothesConverter::toMapMarkerDTO)
113+
.collect(Collectors.toList());
114+
}
80115
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,68 @@ public static class CreateClothesRequest {
7575
@Schema(description = "전시 이미지 목록", example = "[\"https://example.com/image1.jpg\", \"https://example.com/image2.jpg\"]")
7676
private List<String> displayImages;
7777
}
78+
79+
80+
@Getter
81+
@NoArgsConstructor
82+
public static class UpdateRequest {
83+
84+
@NotNull(message = "옷 ID는 필수입니다.")
85+
@Schema(description = "옷 ID", example = "1")
86+
private Long clothesId;
87+
88+
//값이 넘어왔을 때 공백이나 빈 문자열 허용하지 않도록 작성.
89+
// 이미지, 카테고리, 스타일만 수정할 수 없고 나머지는 수정할 수 있음.
90+
91+
@NotBlank(message = "제목은 공백일 수 없습니다.")
92+
private String title;
93+
94+
@Schema(description = "수정할 가격", example = "35000")
95+
private Integer price;
96+
97+
@NotBlank(message = "코멘트는 공백일 수 없습니다.")
98+
private String comment;
99+
100+
@NotBlank(message = "주소는 공백일 수 없습니다.")
101+
private String address;
102+
103+
private Double lat;
104+
private Double lng;
105+
106+
private Boolean saleAgreed;
107+
private Boolean meetupAgreed;
108+
private Boolean offerAgreed;
109+
110+
// 사이즈 정보
111+
@Schema(description = "총장", example = "100")
112+
private String totalLength;
113+
@Schema(description = "가슴 단면", example = "50")
114+
private String chestWidth;
115+
@Schema(description = "어깨 너비", example = "45")
116+
private String shoulderWidth;
117+
@Schema(description = "발 사이즈", example = "270")
118+
private String footSize;
119+
@Schema(description = "허리 단면", example = "40")
120+
private String waistMeasurement;
121+
@Schema(description = "허벅지 단면", example = "30")
122+
private String thighMeasurement;
123+
124+
// 판매 정보
125+
private LocalDate salesPeriod;
126+
private Long discountRate;
127+
128+
@Schema(description = "수정할 피팅 이미지", example = "data:image/jpeg;base64,...")
129+
private String fittingImage;
130+
131+
@Schema(description = "수정할 전시 이미지 목록", example = "[\"data:image/jpeg;base64,...\", \"https://existing-url.com/img.jpg\"]")
132+
private List<String> displayImages;
133+
}
134+
135+
@Getter
136+
@NoArgsConstructor
137+
public static class DeleteRequest {
138+
@NotNull(message = "옷 ID는 필수입니다.")
139+
@Schema(description = "옷 ID", example = "1")
140+
private Long clothesId;
141+
}
78142
}

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import lombok.Builder;
66
import lombok.Getter;
77
import lombok.NoArgsConstructor;
8+
import org.springframework.stereotype.Component;
89

910
import java.time.LocalDateTime;
1011
import java.util.List;
@@ -61,4 +62,61 @@ public static class ClothesPageDTO {
6162
@Schema(description = "마지막 페이지 여부", example = "false")
6263
private Boolean isLast; // 마지막 페이지 여부
6364
}
65+
66+
@Builder
67+
@Getter
68+
@NoArgsConstructor
69+
@AllArgsConstructor
70+
71+
public static class UpdateResponse {
72+
@Schema(description = "수정 후 제목", example = "업데이트된 제목")
73+
private String title;
74+
@Schema(description = "수정 후 코멘트")
75+
private String comment;
76+
@Schema(description = "수정 후 주소")
77+
private String address;
78+
@Schema(description = "수정 후 가격")
79+
private Integer price;
80+
@Schema(description = "수정된 판매글 id", example = "8")
81+
private Long clothesId;
82+
@Schema(description = "수정 후 총 길이", example = "100")
83+
private String totalLength;
84+
@Schema(description = "수정 후 가슴 단면", example = "55")
85+
private String chestWidth;
86+
@Schema(description = "수정 후 어꺠 너비", example = "61")
87+
private String shoulderWidth;
88+
@Schema(description = "수정 후 발 사이즈", example = "270")
89+
private String footSize;
90+
@Schema(description = "수정 후 허리 단면", example = "40")
91+
private String waistMeasurement;
92+
@Schema(description = "수정 후 허벅지 단면", example = "50")
93+
private String thighMeasurement;
94+
95+
@Schema(description = "수정 시각", example = "2025-11-30T10:30:00")
96+
private LocalDateTime updatedAt;
97+
}
98+
99+
//지도 마커용
100+
public record MapMarkerDTO(
101+
@Schema(description = "상품 ID")
102+
Long clothesId,
103+
104+
@Schema(description = "위도")
105+
Double lat,
106+
107+
@Schema(description = "경도")
108+
Double lng,
109+
110+
@Schema(description = "썸네일 이미지 URL")
111+
String imageUrl,
112+
113+
@Schema(description = "상품 제목")
114+
String title,
115+
116+
@Schema(description = "가격")
117+
Integer price,
118+
119+
@Schema(description = "주소")
120+
String address
121+
){}
64122
}

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import jakarta.persistence.*;
88
import lombok.*;
99
import org.hibernate.annotations.ColumnDefault;
10+
import org.springframework.util.StringUtils;
1011

1112
import java.time.LocalDate;
1213
import java.time.LocalDateTime;
@@ -41,7 +42,7 @@ public class Clothes extends BaseEntity {
4142
private Integer price; // 가격
4243

4344
// S3 'fitting/' 폴더에 저장된 URL
44-
@Column(name = "fitting_image", nullable = false)
45+
@Column(name = "fitting_image")
4546
private String fittingImage;
4647

4748
// S3 'display/' 폴더에 저장된 URL
@@ -120,4 +121,57 @@ public void addImage(ClothesImage clothesImage) {
120121
clothesImage.updateClothes(this);
121122
}
122123

124+
public void updateFittingImage(String fittingImage) {
125+
this.fittingImage = fittingImage;
126+
}
127+
128+
//판매글 수정 메서드.
129+
//DTO에서 null이나 공백으로 넘어온 필드는 기존 값 유지.
130+
//수정 불가능 필드 : seller, category, style, fittingImage, images, isSold, soldDate...
131+
public void update(String title, Integer price, String comment,
132+
String address, Double lat, Double lng, Boolean saleAgreed,
133+
Boolean meetupAgreed, Boolean offerAgreed, String totalLength,
134+
String chestWidth, String shoulderWidth, String footSize, String waistMeasurement, String thighMeasurement,
135+
LocalDate salesPeriod, Long discountRate) {
136+
137+
//null과 " " 방어.
138+
if(StringUtils.hasText(title)) {
139+
this.title = title;
140+
}
141+
142+
if(StringUtils.hasText(comment)) {
143+
this.comment = comment;
144+
}
145+
146+
if(StringUtils.hasText(address)) {
147+
this.address = address;
148+
}
149+
150+
if(StringUtils.hasText(totalLength)) {this.totalLength = totalLength;}
151+
if(StringUtils.hasText(chestWidth)) {this.chestWidth = chestWidth;}
152+
if(StringUtils.hasText(shoulderWidth)) {this.shoulderWidth = shoulderWidth;}
153+
if(StringUtils.hasText(footSize)) {this.footSize = footSize;}
154+
if(StringUtils.hasText(waistMeasurement)) {this.waistMeasurement = waistMeasurement;}
155+
if(StringUtils.hasText(thighMeasurement)) {this.thighMeasurement = thighMeasurement;}
156+
157+
// null만 체크 수행.
158+
if(price != null) {this.price = price;}
159+
if(lat != null) {this.lat = lat;}
160+
if(lng != null) {this.lng = lng;}
161+
162+
if(salesPeriod != null) {this.salesPeriod = salesPeriod;}
163+
if(discountRate != null) {this.discountRate = discountRate;}
164+
if(saleAgreed != null) {this.saleAgreed = saleAgreed;}
165+
if(meetupAgreed != null) {this.meetupAgreed = meetupAgreed;}
166+
if(offerAgreed != null) {this.offerAgreed = offerAgreed;}
167+
}
168+
//상태 변경 편의 메서드
169+
public void completeMatching() {
170+
this.isMatched = true;
171+
}
172+
173+
public void completeSelling() {
174+
this.isSold = true;
175+
}
176+
123177
}

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

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

6+
import java.util.List;
7+
68
public interface ClothesRepository extends JpaRepository<Clothes, Long>, ClothesRepositoryCustom {
9+
10+
// 판매 중인(isSold=false) 옷을 모두 가져옴
11+
List<Clothes> findAllByIsSoldFalse();
712
}

0 commit comments

Comments
 (0)