|
| 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