Skip to content

Commit 3543e4e

Browse files
authored
Merge pull request #38 from NET-ZERO-FitFit/develop
main <- dev
2 parents 365592c + c98e345 commit 3543e4e

26 files changed

Lines changed: 1029 additions & 29 deletions

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import fitfit.global.enums.Style;
1111
import org.springframework.data.domain.Page;
1212

13+
import java.util.ArrayList;
1314
import java.util.List;
1415
import java.util.stream.Collectors;
1516

@@ -135,9 +136,18 @@ public static ClothesResponseDTO.ClothesDetailDTO toClothesDetailDTO(Clothes clo
135136
if (Boolean.TRUE.equals(clothes.getIsSold())) status = "SOLD_OUT";
136137
else if (Boolean.TRUE.equals(clothes.getIsMatched())) status = "MATCHED";
137138

138-
List<String> imageUrls = clothes.getImages().stream()
139+
List<String> combinedImages = new ArrayList<>();
140+
141+
// 1. 피팅 이미지 (있으면 맨 앞에 추가)
142+
if (clothes.getFittingImage() != null && !clothes.getFittingImage().isEmpty()) {
143+
combinedImages.add(clothes.getFittingImage());
144+
}
145+
146+
// 2. 상세 이미지들 (뒤에 이어 붙이기)
147+
List<String> detailImages = clothes.getImages().stream()
139148
.map(ClothesImage::getImageUrl)
140149
.toList();
150+
combinedImages.addAll(detailImages);
141151

142152
boolean isDiscounted = clothes.getIsDiscounted();
143153

@@ -156,7 +166,7 @@ public static ClothesResponseDTO.ClothesDetailDTO toClothesDetailDTO(Clothes clo
156166
.sellerProfileUrl(seller.getProfileImgUrl())
157167
.sellerCleanIndex(seller.getCleanIndex() != null ? seller.getCleanIndex() : 50)
158168
// 이미지
159-
.images(imageUrls)
169+
.images(combinedImages)
160170
// 상세
161171
.comment(clothes.getComment())
162172
.style(clothes.getStyle() != null ? clothes.getStyle().toString() : null)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public record ClothesDetailDTO(
164164
String sellerProfileUrl,
165165

166166
@Schema(description = "판매자 매너온도 (클린지수)", example = "90")
167-
Integer sellerCleanIndex,
167+
Double sellerCleanIndex,
168168

169169
//이미지
170170
@Schema(description = "상품 이미지 리스트 (슬라이드용)")

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ public void completeMatching() {
184184
}
185185

186186
public void completeSelling() {
187-
this.isSold = true;
187+
this.isSold = true; this.soldDate = LocalDateTime.now();
188+
}
189+
public void cancelMatching(){
190+
this.isMatched = false;
188191
}
189192

190193
public void increateSeeCount() {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fitfit.domain.clothes.repository;
22

33
import fitfit.domain.clothes.entity.Clothes;
4+
import fitfit.domain.member.entity.Member;
45
import org.springframework.data.domain.Page;
56
import org.springframework.data.domain.Pageable;
67
import org.springframework.data.jpa.repository.JpaRepository;
@@ -17,6 +18,9 @@ public interface ClothesRepository extends JpaRepository<Clothes, Long>, Clothes
1718

1819
Page<Clothes> findByIsSoldFalseOrderBySeeCountDescCreatedAtDesc(Pageable pageable);
1920

21+
//내가 판매 중인 옷 조회(판매 안됨 + 예약 안됨)
22+
//즉 글만 올리고 아무런 주문도 매칭도 안 된 상태
23+
List<Clothes> findAllBySellerAndIsSoldFalseAndIsMatchedFalseOrderByCreatedAtDesc(Member seller);
2024

2125

2226
}

src/main/java/fitfit/domain/member/converter/MemberConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static Member toMember (MemberDataDTO.MemberData kakaoMemberData, Provide
2727
.height("임시 키")
2828
.weight("임시 체중")
2929
.point(0)
30-
.cleanIndex(50)
30+
.cleanIndex(50.0)
3131
.build();
3232
}
3333

src/main/java/fitfit/domain/member/dto/MemberResponseDTO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public record MyProfileResDTO(
7878
String profileImgUrl,
7979

8080
@Schema(description = "클린 지수 (0~100점 사이의 매너 점수)", example = "90")
81-
Integer cleanIndex
81+
Double cleanIndex
8282
){}
8383

8484
@Builder

src/main/java/fitfit/domain/member/entity/Member.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package fitfit.domain.member.entity;
22

3+
import fitfit.global.apiPayload.code.status.ErrorStatus;
4+
import fitfit.global.apiPayload.exception.handler.MemberHandler;
35
import fitfit.global.entity.BaseEntity;
46
import fitfit.global.enums.Gender;
57
import fitfit.global.enums.MemberStatus;
@@ -78,7 +80,9 @@ public class Member extends BaseEntity {
7880

7981
private Integer point;
8082

81-
private Integer cleanIndex;
83+
@Column(columnDefinition = "double default 50.0")
84+
private Double cleanIndex = 50.0;
85+
8286

8387
public void updateNickname(String nickname) {
8488
this.nickname = nickname;
@@ -137,4 +141,35 @@ public void addPoint(Integer amount) {
137141
public void clearInactiveAt() {
138142
this.inactiveAt = null;
139143
}
144+
145+
public void deductPoint(Integer amount){
146+
if(this.point <amount){
147+
throw new MemberHandler(ErrorStatus.MEMBER_NOT_ENOUGH_POINT);
148+
}
149+
this.point-=amount;
150+
}
151+
152+
public void updateCleanIndex(Integer score){
153+
if(this.cleanIndex==null) this.cleanIndex=50.0;
154+
155+
double pointToAdd = 0.0;
156+
157+
switch(score){
158+
case 5 -> pointToAdd = 1.5;
159+
case 4 -> pointToAdd = 0.8;
160+
case 3 -> pointToAdd = 0.0;
161+
case 2 -> pointToAdd = -2.0;
162+
case 1 -> pointToAdd = -5.0;
163+
default -> pointToAdd = 0.0;
164+
}
165+
166+
this.cleanIndex+=pointToAdd;
167+
168+
if(this.cleanIndex>100.0){
169+
this.cleanIndex=100.0;
170+
}
171+
else if(this.cleanIndex<0.0){
172+
this.cleanIndex=0.0;
173+
}
174+
}
140175
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package fitfit.domain.order.controller;
2+
3+
import fitfit.domain.order.dto.OrderRequestDTO;
4+
import fitfit.domain.order.dto.OrderResponseDTO;
5+
import fitfit.domain.order.service.OrderCommandService;
6+
import fitfit.domain.order.service.OrderQueryService;
7+
import fitfit.global.apiPayload.ApiResponse;
8+
import fitfit.global.apiPayload.code.status.SuccessStatus;
9+
import io.swagger.v3.oas.annotations.Operation;
10+
import io.swagger.v3.oas.annotations.Parameter;
11+
import io.swagger.v3.oas.annotations.media.Content;
12+
import io.swagger.v3.oas.annotations.media.Schema;
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import jakarta.validation.Valid;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.web.bind.annotation.*;
17+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
18+
19+
@RestController
20+
@RequestMapping("/api/orders")
21+
@RequiredArgsConstructor
22+
@Tag(name = "Order", description = "거래(주문) 프로세스 관련 API")
23+
public class OrderRestController {
24+
25+
private final OrderCommandService orderCommandService;
26+
private final OrderQueryService orderQueryService;
27+
28+
@PostMapping
29+
@Operation(
30+
summary = "구매 희망 (주문 생성 및 채팅방 연결)",
31+
description = """
32+
상세 페이지에서 '구매 희망' 버튼을 클릭했을 때 호출합니다.
33+
34+
**[로직 설명]**
35+
1. 판매자와 구매자 간의 **채팅방이 생성(또는 조회)**됩니다.
36+
- 채팅방은 생성 즉시 **활성화(isActive=true)** 상태이므로 채팅 목록에 노출됩니다.
37+
2. **주문(Order)** 데이터가 생성되고 상태는 `REQUESTED(요청됨)`가 됩니다.
38+
39+
**[프론트엔드 처리 가이드]**
40+
- 응답의 `status`가 **'REQUESTED'** 인 경우:
41+
-> 채팅방에는 입장시키되, **메시지 입력창을 비활성화** 하고 '판매자의 수락 대기 중' 문구를 띄워주세요.
42+
- 나중에 판매자가 수락하면 `MATCHED` 상태로 변하고 입력을 풀면 됩니다.
43+
"""
44+
)
45+
@ApiResponses({
46+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "주문 생성 성공 (채팅방 생성 완료)", content = @Content(schema = @Schema(implementation = OrderResponseDTO.CreateOrderResultDTO.class))),
47+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "자신의 상품은 구매할 수 없음 (ORDER4003)"),
48+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 진행 중인 거래가 있음 (ORDER4002)")
49+
})
50+
public ApiResponse<OrderResponseDTO.CreateOrderResultDTO> createOrder(
51+
@Parameter(description = "Access Token", required = true)
52+
@RequestHeader("Authorization") String token,
53+
@RequestBody OrderRequestDTO.CreateOrderDTO request
54+
) {
55+
OrderResponseDTO.CreateOrderResultDTO result = orderCommandService.createOrder(token, request);
56+
57+
// 201 Created 상태코드로 응답
58+
return ApiResponse.of(SuccessStatus._OK, result);
59+
}
60+
61+
@PatchMapping("/{orderId}/accept")
62+
@Operation(
63+
summary = "구매 수락 (예약 확정)",
64+
description = """
65+
판매자가 구매 요청을 수락합니다.
66+
67+
**[로직 설명]**
68+
1. 주문 상태가 `MATCHED(예약중)`으로 변경됩니다.
69+
2. 해당 옷의 상태가 `예약중`으로 변경되어 다른 사람이 구매할 수 없게 됩니다.
70+
71+
**[프론트엔드 처리 가이드]**
72+
- 응답의 `status`가 **'MATCHED'** 로 변경된 것을 확인하면:
73+
-> 기존에 막아두었던 **채팅 입력창을 활성화(Unlock)** 하여 대화가 가능하게 해주세요.
74+
75+
**[주의]** 반드시 해당 상품의 **판매자**만 호출할 수 있습니다.
76+
"""
77+
)
78+
@ApiResponses({
79+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "수락 성공 (채팅방 열림)"),
80+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "판매자가 아님 (COMMON401)"),
81+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "주문을 찾을 수 없음 (ORDER4001)")
82+
})
83+
public ApiResponse<OrderResponseDTO.OrderStatusDTO> acceptOrder(
84+
@RequestHeader("Authorization") String token,
85+
@Parameter(description = "수락할 주문 ID") @PathVariable(name = "orderId") Long orderId
86+
) {
87+
OrderResponseDTO.OrderStatusDTO result = orderCommandService.acceptOrder(token, orderId);
88+
return ApiResponse.of(SuccessStatus._OK, result);
89+
}
90+
91+
@PostMapping("/{orderId}/pay")
92+
@Operation(
93+
summary = "결제하기 (포인트 차감)",
94+
description = """
95+
구매자가 '결제하기' 버튼을 눌러 포인트를 사용하고 결제를 완료합니다.
96+
97+
**[로직 설명]**
98+
1. 구매자의 보유 포인트에서 `usedPoints`만큼 차감됩니다.
99+
2. 주문 정보에 `isPaid=true`와 사용한 포인트 양이 기록됩니다.
100+
3. 주문 상태(`status`)는 변하지 않고 `MATCHED`로 유지됩니다. (아직 물건 못 받았으니깐 그런거임 둘다 잘 받았어요 잘 줬어요 누르면 그떄 상태바껴)
101+
102+
**[프론트엔드 처리 가이드]**
103+
- 결제 성공(200) 응답을 받으면, 결제하기 버튼을 비활성화
104+
"""
105+
)
106+
@ApiResponses({
107+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "결제 성공"),
108+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "포인트 부족 (MEMBER4002) 또는 이미 결제됨"),
109+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "주문 없음 (ORDER4001)")
110+
})
111+
public ApiResponse<OrderResponseDTO.OrderStatusDTO> payOrder(
112+
@RequestHeader("Authorization") String token,
113+
@PathVariable(name = "orderId") Long orderId,
114+
@RequestBody OrderRequestDTO.PayOrderDTO request
115+
) {
116+
OrderResponseDTO.OrderStatusDTO result = orderCommandService.payOrder(token, orderId, request);
117+
return ApiResponse.of(SuccessStatus._OK, result);
118+
}
119+
120+
@PatchMapping("/{orderId}/confirm")
121+
@Operation(
122+
summary = "수령/전달 확인 (거래 확정)",
123+
description = """
124+
구매자 혹은 판매자가 '상품을 잘 받았어요', '상품을 잘 전달했어요' 버튼을 눌러 거래를 확정합니다.
125+
126+
**[로직 설명]**
127+
1. **반드시 결제(`isPaid=true`)가 선행되어야 호출 가능합니다.**
128+
2. 호출한 사용자가 누구냐에 따라 확인 플래그(`buyerConfirmed` / `sellerConfirmed`)가 `true`로 변합니다.
129+
3. **두 사람 모두 확인**하면 주문 상태가 `COMPLETED`로 변하고, 옷은 `판매 완료` 처리됩니다.
130+
131+
**[프론트엔드 처리 가이드]**
132+
API 응답의 `status` 값을 확인해서 분기 처리를 해주세요.
133+
"""
134+
)
135+
@ApiResponses({
136+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "확인 처리 성공"),
137+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "결제 안 됨 (ORDER400?)")
138+
})
139+
public ApiResponse<OrderResponseDTO.OrderStatusDTO> confirmOrder(
140+
@RequestHeader("Authorization") String token,
141+
@PathVariable(name = "orderId") Long orderId
142+
) {
143+
OrderResponseDTO.OrderStatusDTO result = orderCommandService.confirmOrder(token, orderId);
144+
return ApiResponse.of(SuccessStatus._OK, result);
145+
}
146+
147+
@GetMapping("/history")
148+
@Operation(
149+
summary = "내 거래 내역 조회 (구매/판매)",
150+
description = """
151+
나의 구매 내역 또는 판매 내역을 상태별로 조회합니다.
152+
153+
**[Query Parameters]**
154+
* `role`: **BUYER** (구매자) 또는 **SELLER** (판매자)
155+
* `state`: 조회할 탭의 상태 (아래 참조)
156+
157+
**[State 상세 옵션]**
158+
1. **BUYER (구매자)**
159+
- `REQUESTED`: **구매중** (구매 요청-> 구매수락 된 상태)
160+
- `PAID`: **결제 완료** (결제된 상태)
161+
- `COMPLETED`: **구매 완료** (서로 잘 보냈어요/잘 받았어요 한 상태)
162+
163+
2. **SELLER (판매자)**
164+
- `ON_SALE`: **판매중** (아직 주문 없는 내 옷들 - Clothes 조회)
165+
- `MATCHED`: **매칭 확정** (예약됨 + 결제된 건 포함)
166+
- `COMPLETED`: **판매 완료** (거래 종료)
167+
"""
168+
)
169+
public ApiResponse<OrderResponseDTO.OrderHistoryListDTO> getOrderHistory(
170+
@RequestHeader("Authorization") String token,
171+
@RequestParam(name = "role") String role,
172+
@RequestParam(name = "state") String state
173+
) {
174+
OrderResponseDTO.OrderHistoryListDTO result = orderQueryService.getOrderHistory(token, role, state);
175+
return ApiResponse.of(SuccessStatus._OK, result);
176+
}
177+
178+
@DeleteMapping("/{orderId}")
179+
@Operation(
180+
summary = "구매 취소 (요청 철회 / 예약 취소)",
181+
description = """
182+
구매자가 거래를 취소합니다.
183+
184+
* **취소 가능 상태:**
185+
1. `REQUESTED` (구매 요청 단계): 즉시 철회됩니다.
186+
2. `MATCHED` (매칭/예약 단계): **결제 전이라면** 취소 가능합니다.
187+
- 이때 옷(`Clothes`)의 예약 상태(`isMatched`)가 해제되어 다시 판매 중(`ON_SALE`)으로 돌아갑니다.
188+
189+
* **취소 불가 상태:**
190+
- 이미 결제(`PAID`)했거나 거래 완료(`COMPLETED`)된 경우 에러를 반환합니다.
191+
"""
192+
)
193+
@ApiResponses({
194+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "취소 성공"),
195+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "결제됨/완료됨/권한없음 등의 사유로 실패")
196+
})
197+
public ApiResponse<String> cancelOrder(
198+
@RequestHeader("Authorization") String token,
199+
@PathVariable(name = "orderId") Long orderId
200+
) {
201+
orderCommandService.cancelOrder(token, orderId);
202+
return ApiResponse.of(SuccessStatus._OK, "주문이 성공적으로 취소되었습니다.");
203+
}
204+
205+
@PostMapping("/{orderId}/review")
206+
@Operation(
207+
summary = "리뷰 작성 (클린지수 평가)",
208+
description = """
209+
거래 완료(`COMPLETED`) 후 구매자가 판매자를 평가합니다.
210+
입력된 점수(1~5)에 따라 판매자의 클린지수가 변동됩니다.
211+
212+
**[점수 반영 정책]**
213+
* 💧 5점: **+1.5점** (최고예요)
214+
* 💧 4점: **+0.8점** (좋아요)
215+
* 💧 3점: **변동 없음** (보통이에요)
216+
* 💧 2점: **-2.0점** (아쉬워요)
217+
* 💧 1점: **-5.0점** (별로예요)
218+
"""
219+
)
220+
@ApiResponses({
221+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "리뷰 반영 성공"),
222+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "완료된 거래가 아니거나 이미 작성함 (ORDER4007, ORDER4008)")
223+
})
224+
public ApiResponse<Long> writeReview(
225+
@RequestHeader("Authorization") String token,
226+
@PathVariable(name = "orderId") Long orderId,
227+
@RequestBody @Valid OrderRequestDTO.ReviewDTO request
228+
) {
229+
Long resultId = orderCommandService.writeReview(token, orderId, request);
230+
return ApiResponse.of(SuccessStatus._OK, resultId);
231+
}
232+
}

0 commit comments

Comments
 (0)