Skip to content

Commit 0953743

Browse files
authored
[Refactor]: 상품 도메인 인터페이스 확장성 도입 (#196)
* [Feat]: SecurityConfig, .env.default 수정 * [Refactor]: Product 수정 로직 개선 * [Fix]: ProductServiceTest 수정 케이스 수정 * [Comment]: Controller 주석을 인터페이스로 이동 * [Refactor]: ProductService 인터페이스 도입 * [Style]: Service 메서드 위치 변경 * [Feat]: 기작 추가 * [Feat]: Product를 추상 클래스로 적용 * [Feat]: Facade 패턴 적용
1 parent ea875b3 commit 0953743

28 files changed

+1073
-412
lines changed

.env.default

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PG_TOSS_CLIENT_KEY=NEED_TO_SET
22
PG_TOSS_SECRET_KEY=NEED_TO_SET
3-
SPRING__DATA__REDIS__PASSWORD=NEED_TO_SET
4-
SPRING__DATASOURCE__URL___DB_NAME=NEED_TO_SET
3+
SPRING_DATA_REDIS_PASSWORD=NEED_TO_SET
4+
SPRING_DATASOURCE_URL___DB_NAME=NEED_TO_SET
55
JWT_SECRET=NEED_TO_SET
66
JWT_ACCESS_TOKEN_EXPIRATION=NEED_TO_SET
77
JWT_REFRESH_TOKEN_EXPIRATION=NEED_TO_SET
Lines changed: 12 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
package com.backend.domain.product.controller;
22

3-
import com.backend.domain.member.entity.Member;
4-
import com.backend.domain.member.service.MemberService;
5-
import com.backend.domain.product.document.ProductDocument;
63
import com.backend.domain.product.dto.ProductSearchDto;
74
import com.backend.domain.product.dto.request.ProductCreateRequest;
85
import com.backend.domain.product.dto.request.ProductModifyRequest;
96
import com.backend.domain.product.dto.response.*;
10-
import com.backend.domain.product.entity.Product;
117
import com.backend.domain.product.enums.AuctionStatus;
128
import com.backend.domain.product.enums.ProductSearchSortType;
139
import com.backend.domain.product.enums.SaleStatus;
14-
import com.backend.domain.product.exception.ProductException;
15-
import com.backend.domain.product.mapper.ProductMapper;
16-
import com.backend.domain.product.service.ProductSearchService;
17-
import com.backend.domain.product.service.ProductService;
10+
import com.backend.domain.product.facade.ProductFacade;
1811
import com.backend.global.page.dto.PageDto;
1912
import com.backend.global.response.RsData;
2013
import jakarta.validation.Valid;
2114
import lombok.RequiredArgsConstructor;
22-
import org.springframework.data.domain.Page;
2315
import org.springframework.http.MediaType;
2416
import org.springframework.security.core.annotation.AuthenticationPrincipal;
2517
import org.springframework.security.core.userdetails.User;
@@ -29,60 +21,23 @@
2921

3022
import java.util.List;
3123

32-
/**
33-
* 상품 관련 REST API 컨트롤러
34-
* - 경매 상품의 CRUD 작업을 처리
35-
* - RDB와 Elasticsearch 기반 검색 기능 제공
36-
* - 멀티파트 파일 업로드를 통한 이미지 처리
37-
*/
3824
@RestController
3925
@RequiredArgsConstructor
4026
public class ApiV1ProductController implements ApiV1ProductControllerDocs {
41-
private final ProductService productService;
42-
private final MemberService memberService;
43-
private final ProductMapper productMapper;
44-
private final ProductSearchService productSearchService;
27+
private final ProductFacade productFacade;
4528

46-
/**
47-
* 상품 등록
48-
* - 상품 정보와 이미지를 함께 업로드하여 새 경매 상품 생성
49-
* - 이미지는 최소 1개, 최대 5개까지 업로드 가능
50-
* - 상품 생성 시 Elasticsearch에도 자동으로 인덱싱됨
51-
*
52-
* @param request 상품 등록 요청 정보 (JSON)
53-
* @param images 상품 이미지 파일 리스트 (최소 1개, 최대 5개)
54-
* @param user 현재 로그인한 사용자 정보
55-
* @return 생성된 상품의 상세 정보
56-
*/
5729
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
5830
@Transactional
5931
public RsData<ProductResponse> createProduct(
32+
@RequestParam(defaultValue = "STANDARD") String productType,
6033
@RequestPart("request") @Valid ProductCreateRequest request,
6134
@RequestPart("images") List<MultipartFile> images,
6235
@AuthenticationPrincipal User user
6336
) {
64-
Member actor = memberService.findMemberByEmail(user.getUsername());
65-
Product product = productService.createProduct(actor, request, images);
66-
67-
ProductResponse response = productMapper.toResponse(product);
37+
ProductResponse response = productFacade.createProduct(productType, request, images, user);
6838
return RsData.created("상품이 등록되었습니다", response);
6939
}
7040

71-
/**
72-
* 상품 목록 조회 (RDB 기반)
73-
* - 다양한 필터 조건으로 상품 검색
74-
* - QueryDSL을 사용한 동적 쿼리 생성
75-
*
76-
* @param page 페이지 번호 (1부터 시작, 기본값: 1)
77-
* @param size 페이지 크기 (기본값: 20, 최대: 100)
78-
* @param keyword 상품명 검색 키워드
79-
* @param category 카테고리 ID 배열 (복수 선택 가능)
80-
* @param location 거래 지역 배열 (복수 선택 가능)
81-
* @param isDelivery 택배 가능 여부 필터
82-
* @param status 경매 상태 (BIDDING, BEFORE_START 등)
83-
* @param sort 정렬 기준 (LATEST, PRICE_HIGH, PRICE_LOW, ENDING_SOON, POPULAR)
84-
* @return 페이징된 상품 목록
85-
*/
8641
@GetMapping
8742
@Transactional(readOnly = true)
8843
public RsData<PageDto<ProductListItemDto>> getProducts(
@@ -96,18 +51,10 @@ public RsData<PageDto<ProductListItemDto>> getProducts(
9651
@RequestParam(defaultValue = "LATEST") ProductSearchSortType sort
9752
) {
9853
ProductSearchDto search = new ProductSearchDto(keyword, category, location, isDelivery, status);
99-
Page<Product> products = productService.findBySearchPaged(page, size, sort, search);
100-
101-
PageDto<ProductListItemDto> response = productMapper.toListResponse(products);
54+
PageDto<ProductListItemDto> response = productFacade.getProducts(page, size, sort, search);
10255
return RsData.ok("상품 목록이 조회되었습니다", response);
10356
}
10457

105-
/**
106-
* 상품 목록 조회 (Elasticsearch 기반)
107-
* - Elasticsearch의 전문 검색 기능 활용
108-
* - 한글 형태소 분석(nori analyzer) 지원
109-
* - RDB보다 빠른 검색 성능 제공
110-
*/
11158
@GetMapping("/es")
11259
@Transactional(readOnly = true)
11360
public RsData<PageDto<ProductListItemDto>> getProductsByElasticsearch(
@@ -121,42 +68,17 @@ public RsData<PageDto<ProductListItemDto>> getProductsByElasticsearch(
12168
@RequestParam(required = false) ProductSearchSortType sort
12269
) {
12370
ProductSearchDto search = new ProductSearchDto(keyword, category, location, isDelivery, status);
124-
Page<ProductDocument> products = productSearchService.searchProducts(page, size, sort, search);
125-
126-
PageDto<ProductListItemDto> response = productMapper.toListResponseFromDocument(products);
71+
PageDto<ProductListItemDto> response = productFacade.getProductsByElasticsearch(page, size, sort, search);
12772
return RsData.ok("상품 목록이 조회되었습니다", response);
12873
}
12974

130-
/**
131-
* 상품 상세 조회
132-
* - 특정 상품의 모든 정보를 조회
133-
* - 이미지 목록, 판매자 정보 포함
134-
*
135-
* @param productId 조회할 상품의 ID
136-
* @return 상품 상세 정보
137-
*/
13875
@GetMapping("/{productId}")
13976
@Transactional(readOnly = true)
14077
public RsData<ProductResponse> getProduct(@PathVariable Long productId) {
141-
Product product = productService.getProductById(productId);
142-
143-
ProductResponse response = productMapper.toResponse(product);
78+
ProductResponse response = productFacade.getProduct(productId);
14479
return RsData.ok("상품이 조회되었습니다", response);
14580
}
14681

147-
/**
148-
* 상품 수정
149-
* - 상품 정보 수정 및 이미지 추가/삭제
150-
* - 경매 시작 전에만 수정 가능
151-
* - 본인의 상품만 수정 가능
152-
*
153-
* @param productId 수정할 상품의 ID
154-
* @param request 수정할 상품 정보 (변경할 필드만 포함)
155-
* @param images 추가할 이미지 파일 (선택)
156-
* @param deleteImageIds 삭제할 이미지 ID 리스트 (선택)
157-
* @param user 현재 로그인한 사용자
158-
* @return 수정된 상품 정보
159-
*/
16082
@PutMapping(value = "/{productId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
16183
@Transactional
16284
public RsData<ProductResponse> modifyProduct(
@@ -166,50 +88,21 @@ public RsData<ProductResponse> modifyProduct(
16688
@RequestPart(value = "deleteImageIds", required = false) List<Long> deleteImageIds,
16789
@AuthenticationPrincipal User user
16890
) {
169-
Member actor = memberService.findMemberByEmail(user.getUsername());
170-
Product product = productService.getProductById(productId);
171-
172-
product.checkActorCanModify(actor);
173-
174-
productService.modifyProduct(product, request, images, deleteImageIds);
175-
176-
ProductResponse response = productMapper.toResponse(product);
91+
ProductResponse response = productFacade.modifyProduct(productId, request, images, deleteImageIds, user);
17792
return RsData.ok("상품이 수정되었습니다", response);
17893
}
17994

180-
/**
181-
* 상품 삭제
182-
* - 경매 시작 전에만 삭제 가능
183-
* - 본인의 상품만 삭제 가능
184-
* - 관련된 모든 이미지 파일도 함께 삭제됨
185-
*
186-
* @param productId 삭제할 상품의 ID
187-
* @param user 현재 로그인한 사용자
188-
*/
18995
@DeleteMapping("/{productId}")
19096
@Transactional
19197
public RsData<Void> deleteProduct(
19298
@PathVariable Long productId,
19399
@AuthenticationPrincipal User user
194100
) {
195-
Member actor = memberService.findMemberByEmail(user.getUsername());
196-
Product product = productService.getProductById(productId);
197-
198-
product.checkActorCanDelete(actor);
199-
200-
productService.deleteProduct(product);
101+
productFacade.deleteProduct(productId, user);
201102

202103
return RsData.ok("상품이 삭제되었습니다");
203104
}
204105

205-
/**
206-
* 내 상품 목록 조회 (RDB 기반)
207-
* - 로그인한 사용자가 등록한 상품 목록 조회
208-
* - 판매 상태별 필터링 가능
209-
* - 낙찰자 및 리뷰 정보 포함
210-
*
211-
* @param status 판매 상태 (SELLING, SOLD, FAILED)
212-
*/
213106
@GetMapping("/me")
214107
@Transactional(readOnly = true)
215108
public RsData<PageDto<MyProductListItemDto>> getMyProducts(
@@ -219,22 +112,10 @@ public RsData<PageDto<MyProductListItemDto>> getMyProducts(
219112
@RequestParam(defaultValue = "LATEST") ProductSearchSortType sort,
220113
@AuthenticationPrincipal User user
221114
) {
222-
Member actor = memberService.findMemberByEmail(user.getUsername());
223-
Page<Product> products = productService.findByMemberPaged(page, size, sort, actor, status);
224-
225-
PageDto<MyProductListItemDto> response = productMapper.toMyListResponse(products);
115+
PageDto<MyProductListItemDto> response = productFacade.getMyProducts(page, size, sort, status, user);
226116
return RsData.ok("내 상품 목록이 조회되었습니다", response);
227117
}
228118

229-
/**
230-
* 특정 회원의 상품 목록 조회 (RDB 기반)
231-
* - 다른 회원이 등록한 상품 목록 조회
232-
* - 판매 상태별 필터링 가능
233-
* - 리뷰 정보 포함
234-
* - 회원 프로필 페이지 등에서 사용
235-
*
236-
* @param memberId 조회할 회원의 ID
237-
*/
238119
@GetMapping("/members/{memberId}")
239120
@Transactional(readOnly = true)
240121
public RsData<PageDto<ProductListByMemberItemDto>> getProductsByMember(
@@ -244,22 +125,16 @@ public RsData<PageDto<ProductListByMemberItemDto>> getProductsByMember(
244125
@RequestParam(defaultValue = "SELLING") SaleStatus status,
245126
@RequestParam(defaultValue = "LATEST") ProductSearchSortType sort
246127
) {
247-
Member actor = memberService.findById(memberId).orElseThrow(ProductException::memberNotFound);
248-
249-
Page<Product> products = productService.findByMemberPaged(page, size, sort, actor, status);
250-
251-
PageDto<ProductListByMemberItemDto> response = productMapper.toListByMemberResponse(products);
128+
PageDto<ProductListByMemberItemDto> response = productFacade.getProductsByMember(memberId, page, size, sort, status);
252129
return RsData.ok("%d번 회원 상품 목록이 조회되었습니다".formatted(memberId), response);
253130
}
254131

255132
/**
256-
* Elasticsearch 검색 분석기 재로드
257-
* 사용자 사전, 동의어 사전 변경 후 호출 필요
258133
* TODO: 관리자만 접근 가능하도록 변경 필요
259134
*/
260135
@PostMapping("/reload-analyzers")
261136
// @PreAuthorize("hasRole('ADMIN')")
262137
public RsData<ReloadAnalyzersResponse> reloadSearchAnalyzers() {
263-
return productSearchService.reloadSearchAnalyzers();
138+
return productFacade.reloadSearchAnalyzers();
264139
}
265140
}

0 commit comments

Comments
 (0)