Skip to content

Commit b8cb83b

Browse files
authored
[BACKEND] 내 견적 저장/관리 API추가 (#87)
- 여러 견적을 저장/관리할 수 있도록 API 추가 구현 - 엔드포인트 경로 및 파라미터 수정 - 아이템 수량 조정 API 추가
1 parent 4e5a77a commit b8cb83b

File tree

13 files changed

+435
-92
lines changed

13 files changed

+435
-92
lines changed

backend/src/main/java/com/cmg/comtogether/common/exception/ErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public enum ErrorCode {
3838
// 견적
3939
QUOTE_NOT_FOUND(404, "QUOTE-001", "견적을 찾을 수 없습니다."),
4040
QUOTE_ITEM_NOT_FOUND(404, "QUOTE-002", "견적 항목을 찾을 수 없습니다."),
41-
QUOTE_ACCESS_DENIED(403, "QUOTE-003", "견적에 대한 접근 권한이 없습니다.");
41+
QUOTE_ACCESS_DENIED(403, "QUOTE-003", "견적에 대한 접근 권한이 없습니다."),
42+
QUOTE_NAME_REQUIRED(400, "QUOTE-004", "견적 이름이 필요합니다.");
4243

4344
private final int status;
4445
private final String code;

backend/src/main/java/com/cmg/comtogether/quote/controller/QuoteController.java

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,26 @@
55
import com.cmg.comtogether.quote.dto.AddQuoteItemRequestDto;
66
import com.cmg.comtogether.quote.dto.QuoteItemResponseDto;
77
import com.cmg.comtogether.quote.dto.QuoteResponseDto;
8+
import com.cmg.comtogether.quote.dto.QuoteSummaryDto;
9+
import com.cmg.comtogether.quote.dto.SaveQuoteRequestDto;
10+
import com.cmg.comtogether.quote.dto.UpdateQuoteItemQuantityRequestDto;
811
import com.cmg.comtogether.quote.service.QuoteService;
912
import com.cmg.comtogether.user.entity.User;
1013
import jakarta.validation.Valid;
1114
import lombok.RequiredArgsConstructor;
1215
import org.springframework.http.ResponseEntity;
1316
import org.springframework.security.core.annotation.AuthenticationPrincipal;
14-
import org.springframework.web.bind.annotation.*;
17+
import org.springframework.web.bind.annotation.DeleteMapping;
18+
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.PatchMapping;
20+
import org.springframework.web.bind.annotation.PathVariable;
21+
import org.springframework.web.bind.annotation.PostMapping;
22+
import org.springframework.web.bind.annotation.PutMapping;
23+
import org.springframework.web.bind.annotation.RequestBody;
24+
import org.springframework.web.bind.annotation.RequestMapping;
25+
import org.springframework.web.bind.annotation.RestController;
26+
27+
import java.util.List;
1528

1629
@RestController
1730
@RequestMapping("/quotes")
@@ -21,56 +34,122 @@ public class QuoteController {
2134
private final QuoteService quoteService;
2235

2336
/**
24-
* 현재 견적 조회
25-
* GET /quotes
37+
* 새 견적 생성 (초안 상태)
38+
*/
39+
@PostMapping
40+
public ResponseEntity<ApiResponse<QuoteResponseDto>> createQuote(
41+
@AuthenticationPrincipal CustomUserDetails userDetails
42+
) {
43+
User user = userDetails.getUser();
44+
QuoteResponseDto responseDto = quoteService.createQuote(user.getUserId());
45+
return ResponseEntity.ok(ApiResponse.success(responseDto));
46+
}
47+
48+
/**
49+
* 저장된 견적 목록 조회
2650
*/
2751
@GetMapping
28-
public ResponseEntity<ApiResponse<QuoteResponseDto>> getCurrentQuote(
52+
public ResponseEntity<ApiResponse<List<QuoteSummaryDto>>> getQuotes(
2953
@AuthenticationPrincipal CustomUserDetails userDetails
3054
) {
3155
User user = userDetails.getUser();
32-
QuoteResponseDto responseDto = quoteService.getCurrentQuote(user.getUserId());
56+
List<QuoteSummaryDto> response = quoteService.getSavedQuotes(user.getUserId());
57+
return ResponseEntity.ok(ApiResponse.success(response));
58+
}
59+
60+
/**
61+
* 저장된 견적 단건 조회
62+
*/
63+
@GetMapping("/{quoteId:\\d+}")
64+
public ResponseEntity<ApiResponse<QuoteResponseDto>> getQuote(
65+
@AuthenticationPrincipal CustomUserDetails userDetails,
66+
@PathVariable Long quoteId
67+
) {
68+
User user = userDetails.getUser();
69+
QuoteResponseDto responseDto = quoteService.getQuote(user.getUserId(), quoteId);
3370
return ResponseEntity.ok(ApiResponse.success(responseDto));
3471
}
3572

3673
/**
3774
* 견적에 상품 추가
38-
* POST /quotes/items
3975
*/
40-
@PostMapping("/items")
76+
@PostMapping("/{quoteId:\\d+}/items")
4177
public ResponseEntity<ApiResponse<QuoteItemResponseDto>> addItem(
4278
@AuthenticationPrincipal CustomUserDetails userDetails,
79+
@PathVariable Long quoteId,
4380
@Valid @RequestBody AddQuoteItemRequestDto requestDto
4481
) {
4582
User user = userDetails.getUser();
46-
QuoteItemResponseDto responseDto = quoteService.addItem(user.getUserId(), requestDto);
83+
QuoteItemResponseDto responseDto = quoteService.addItem(user.getUserId(), quoteId, requestDto);
4784
return ResponseEntity.ok(ApiResponse.success(responseDto));
4885
}
4986

5087
/**
51-
* 견적에서 상품 삭제
52-
* DELETE /quotes/items/{quoteItemId}
88+
* 견적 상품 수량 수정
5389
*/
54-
@DeleteMapping("/items/{quoteItemId}")
90+
@PatchMapping("/{quoteId:\\d+}/items/{quoteItemId}")
91+
public ResponseEntity<ApiResponse<QuoteItemResponseDto>> updateItemQuantity(
92+
@AuthenticationPrincipal CustomUserDetails userDetails,
93+
@PathVariable Long quoteId,
94+
@PathVariable Long quoteItemId,
95+
@Valid @RequestBody UpdateQuoteItemQuantityRequestDto requestDto
96+
) {
97+
User user = userDetails.getUser();
98+
QuoteItemResponseDto responseDto = quoteService.updateItemQuantity(user.getUserId(), quoteId, quoteItemId, requestDto.getQuantity());
99+
return ResponseEntity.ok(ApiResponse.success(responseDto));
100+
}
101+
102+
/**
103+
* 견적 저장 (이름 지정 필수)
104+
*/
105+
@PutMapping("/{quoteId:\\d+}")
106+
public ResponseEntity<ApiResponse<QuoteResponseDto>> saveQuote(
107+
@AuthenticationPrincipal CustomUserDetails userDetails,
108+
@PathVariable Long quoteId,
109+
@Valid @RequestBody SaveQuoteRequestDto requestDto
110+
) {
111+
User user = userDetails.getUser();
112+
QuoteResponseDto responseDto = quoteService.saveQuote(user.getUserId(), quoteId, requestDto.getName());
113+
return ResponseEntity.ok(ApiResponse.success(responseDto));
114+
}
115+
116+
/**
117+
* 견적에서 특정 상품 삭제
118+
*/
119+
@DeleteMapping("/{quoteId:\\d+}/items/{quoteItemId}")
55120
public ResponseEntity<ApiResponse<QuoteResponseDto>> removeItem(
56121
@AuthenticationPrincipal CustomUserDetails userDetails,
122+
@PathVariable Long quoteId,
57123
@PathVariable Long quoteItemId
58124
) {
59125
User user = userDetails.getUser();
60-
QuoteResponseDto responseDto = quoteService.removeItem(user.getUserId(), quoteItemId);
126+
QuoteResponseDto responseDto = quoteService.removeItem(user.getUserId(), quoteId, quoteItemId);
61127
return ResponseEntity.ok(ApiResponse.success(responseDto));
62128
}
63129

64130
/**
65-
* 견적 전체 비우기
66-
* DELETE /quotes
131+
* 견적의 모든 상품 삭제
67132
*/
68-
@DeleteMapping
133+
@DeleteMapping("/{quoteId:\\d+}/items")
69134
public ResponseEntity<ApiResponse<QuoteResponseDto>> clearQuote(
70-
@AuthenticationPrincipal CustomUserDetails userDetails
135+
@AuthenticationPrincipal CustomUserDetails userDetails,
136+
@PathVariable Long quoteId
71137
) {
72138
User user = userDetails.getUser();
73-
QuoteResponseDto responseDto = quoteService.clearQuote(user.getUserId());
139+
QuoteResponseDto responseDto = quoteService.clearQuote(user.getUserId(), quoteId);
74140
return ResponseEntity.ok(ApiResponse.success(responseDto));
75141
}
142+
143+
/**
144+
* 견적 삭제
145+
*/
146+
@DeleteMapping("/{quoteId:\\d+}")
147+
public ResponseEntity<ApiResponse<Void>> deleteQuote(
148+
@AuthenticationPrincipal CustomUserDetails userDetails,
149+
@PathVariable Long quoteId
150+
) {
151+
User user = userDetails.getUser();
152+
quoteService.deleteQuote(user.getUserId(), quoteId);
153+
return ResponseEntity.ok(ApiResponse.success(null));
154+
}
76155
}

backend/src/main/java/com/cmg/comtogether/quote/dto/AddQuoteItemRequestDto.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.cmg.comtogether.quote.dto;
22

3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import jakarta.validation.constraints.Min;
35
import jakarta.validation.constraints.NotBlank;
46
import jakarta.validation.constraints.NotNull;
57
import lombok.AllArgsConstructor;
@@ -12,6 +14,7 @@
1214
public class AddQuoteItemRequestDto {
1315

1416
@NotNull(message = "상품 ID는 필수입니다.")
17+
@JsonProperty("product_id")
1518
private Long productId;
1619

1720
@NotBlank(message = "상품명은 필수입니다.")
@@ -28,18 +31,27 @@ public class AddQuoteItemRequestDto {
2831
private String link;
2932

3033
@NotBlank(message = "몰명은 필수입니다.")
34+
@JsonProperty("mall_name")
3135
private String mallName;
3236

37+
@JsonProperty("product_type")
3338
private String productType;
3439

3540
private String maker;
3641

3742
private String brand;
3843

3944
// 네이버 쇼핑 카테고리(대분류~세분류)
45+
@JsonProperty("category1")
4046
private String category1;
47+
@JsonProperty("category2")
4148
private String category2;
49+
@JsonProperty("category3")
4250
private String category3;
51+
@JsonProperty("category4")
4352
private String category4;
53+
54+
@Min(value = 1, message = "수량은 1 이상이어야 합니다.")
55+
private Integer quantity = 1;
4456
}
4557

backend/src/main/java/com/cmg/comtogether/quote/dto/QuoteItemResponseDto.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.cmg.comtogether.quote.dto;
22

33
import com.cmg.comtogether.quote.entity.QuoteItem;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
45
import lombok.AllArgsConstructor;
56
import lombok.Builder;
67
import lombok.Getter;
@@ -13,21 +14,31 @@
1314
@AllArgsConstructor
1415
@Builder
1516
public class QuoteItemResponseDto {
17+
@JsonProperty("quote_item_id")
1618
private Long quoteItemId;
19+
@JsonProperty("product_id")
1720
private Long productId;
1821
private String title;
1922
private Integer lprice;
2023
private Integer hprice;
2124
private String image;
2225
private String link;
26+
@JsonProperty("mall_name")
2327
private String mallName;
28+
@JsonProperty("product_type")
2429
private String productType;
2530
private String brand;
31+
@JsonProperty("category1")
2632
private String category1;
33+
@JsonProperty("category2")
2734
private String category2;
35+
@JsonProperty("category3")
2836
private String category3; // 견적 슬롯으로 활용 (예: CPU, 메모리 등)
37+
@JsonProperty("category4")
2938
private String category4;
3039

40+
private Integer quantity;
41+
3142

3243
private LocalDateTime createdAt;
3344

@@ -47,6 +58,7 @@ public static QuoteItemResponseDto from(QuoteItem quoteItem) {
4758
.category2(quoteItem.getCategory2())
4859
.category3(quoteItem.getCategory3())
4960
.category4(quoteItem.getCategory4())
61+
.quantity(quoteItem.getQuantity())
5062
.createdAt(quoteItem.getCreatedAt())
5163
.build();
5264
}

backend/src/main/java/com/cmg/comtogether/quote/dto/QuoteResponseDto.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.cmg.comtogether.quote.dto;
22

33
import com.cmg.comtogether.quote.entity.Quote;
4-
import com.cmg.comtogether.quote.entity.QuoteItem;
54
import lombok.AllArgsConstructor;
65
import lombok.Builder;
76
import lombok.Getter;
@@ -17,29 +16,37 @@
1716
@Builder
1817
public class QuoteResponseDto {
1918
private Long quoteId;
19+
private String name;
20+
private boolean saved;
2021
private List<QuoteItemResponseDto> items;
2122
private LocalDateTime createdAt;
2223
private LocalDateTime updatedAt;
2324
private Integer totalPrice; //최저가(lprice) 합계
24-
private Integer itemCount;
25+
private Integer totalQuantity;
2526

2627
public static QuoteResponseDto from(Quote quote) {
2728
List<QuoteItemResponseDto> items = quote.getItems().stream()
2829
.map(QuoteItemResponseDto::from)
2930
.collect(Collectors.toList());
3031

3132
Integer totalPrice = quote.getItems().stream()
32-
.map(QuoteItem::getLprice)
33-
.filter(price -> price != null)
33+
.filter(item -> item.getLprice() != null)
34+
.map(item -> item.getLprice() * (item.getQuantity() == null ? 1 : item.getQuantity()))
35+
.reduce(0, Integer::sum);
36+
37+
Integer totalQuantity = quote.getItems().stream()
38+
.map(item -> item.getQuantity() == null ? 1 : item.getQuantity())
3439
.reduce(0, Integer::sum);
3540

3641
return QuoteResponseDto.builder()
3742
.quoteId(quote.getQuoteId())
43+
.name(quote.getName())
44+
.saved(quote.isSaved())
3845
.items(items)
3946
.createdAt(quote.getCreatedAt())
4047
.updatedAt(quote.getUpdatedAt())
4148
.totalPrice(totalPrice)
42-
.itemCount(items.size())
49+
.totalQuantity(totalQuantity)
4350
.build();
4451
}
4552
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.cmg.comtogether.quote.dto;
2+
3+
import com.cmg.comtogether.quote.entity.Quote;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.time.LocalDateTime;
10+
11+
@Getter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Builder
15+
public class QuoteSummaryDto {
16+
17+
private Long quoteId;
18+
private String name;
19+
private boolean saved;
20+
private LocalDateTime createdAt;
21+
private LocalDateTime updatedAt;
22+
private Integer totalQuantity;
23+
private Integer totalPrice;
24+
25+
public static QuoteSummaryDto from(Quote quote) {
26+
int totalQuantity = quote.getItems() == null ? 0 :
27+
quote.getItems().stream()
28+
.map(item -> item.getQuantity() == null ? 1 : item.getQuantity())
29+
.reduce(0, Integer::sum);
30+
int totalPrice = quote.getItems() == null ? 0 :
31+
quote.getItems().stream()
32+
.map(item -> {
33+
int price = item.getLprice() == null ? 0 : item.getLprice();
34+
int quantity = item.getQuantity() == null ? 1 : item.getQuantity();
35+
return price * quantity;
36+
})
37+
.reduce(0, Integer::sum);
38+
39+
return QuoteSummaryDto.builder()
40+
.quoteId(quote.getQuoteId())
41+
.name(quote.getName())
42+
.saved(quote.isSaved())
43+
.createdAt(quote.getCreatedAt())
44+
.updatedAt(quote.getUpdatedAt())
45+
.totalQuantity(totalQuantity)
46+
.totalPrice(totalPrice)
47+
.build();
48+
}
49+
}
50+
51+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.cmg.comtogether.quote.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Getter
9+
@NoArgsConstructor
10+
@AllArgsConstructor
11+
public class SaveQuoteRequestDto {
12+
13+
@NotBlank(message = "견적서 이름은 필수입니다.")
14+
private String name;
15+
}
16+
17+

0 commit comments

Comments
 (0)