Skip to content

Commit 80a28b5

Browse files
authored
[Comment]: 상품 도메인 주석 추가 (#163)
* [Comment]: ProductController * [Comment]: ProductService * [Comment]: ProductImageService * [Comment]: ProductSyncService * [Comment]: FileService * [Comment]: ProductSearchService * [Comment]: Product * [Comment]: event 관련 * [Comment]: mapper, document * [Comment]: repository * [Fix]: Payment test 오류 해결
1 parent 89d0a53 commit 80a28b5

File tree

16 files changed

+848
-95
lines changed

16 files changed

+848
-95
lines changed

src/main/java/com/backend/domain/product/controller/ApiV1ProductController.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232

3333
import java.util.List;
3434

35+
/**
36+
* 상품 관련 REST API 컨트롤러
37+
* - 경매 상품의 CRUD 작업을 처리
38+
* - RDB와 Elasticsearch 기반 검색 기능 제공
39+
* - 멀티파트 파일 업로드를 통한 이미지 처리
40+
*/
3541
@RestController
3642
@RequiredArgsConstructor
3743
public class ApiV1ProductController implements ApiV1ProductControllerDocs {
@@ -40,6 +46,17 @@ public class ApiV1ProductController implements ApiV1ProductControllerDocs {
4046
private final ProductMapper productMapper;
4147
private final ProductSearchService productSearchService;
4248

49+
/**
50+
* 상품 등록
51+
* - 상품 정보와 이미지를 함께 업로드하여 새 경매 상품 생성
52+
* - 이미지는 최소 1개, 최대 5개까지 업로드 가능
53+
* - 상품 생성 시 Elasticsearch에도 자동으로 인덱싱됨
54+
*
55+
* @param request 상품 등록 요청 정보 (JSON)
56+
* @param images 상품 이미지 파일 리스트 (최소 1개, 최대 5개)
57+
* @param user 현재 로그인한 사용자 정보
58+
* @return 생성된 상품의 상세 정보
59+
*/
4360
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
4461
@Transactional
4562
public RsData<ProductResponse> createProduct(
@@ -54,6 +71,21 @@ public RsData<ProductResponse> createProduct(
5471
return RsData.created("상품이 등록되었습니다", response);
5572
}
5673

74+
/**
75+
* 상품 목록 조회 (RDB 기반)
76+
* - 다양한 필터 조건으로 상품 검색
77+
* - QueryDSL을 사용한 동적 쿼리 생성
78+
*
79+
* @param page 페이지 번호 (1부터 시작, 기본값: 1)
80+
* @param size 페이지 크기 (기본값: 20, 최대: 100)
81+
* @param keyword 상품명 검색 키워드
82+
* @param category 카테고리 ID 배열 (복수 선택 가능)
83+
* @param location 거래 지역 배열 (복수 선택 가능)
84+
* @param isDelivery 택배 가능 여부 필터
85+
* @param status 경매 상태 (BIDDING, BEFORE_START 등)
86+
* @param sort 정렬 기준 (LATEST, PRICE_HIGH, PRICE_LOW, ENDING_SOON, POPULAR)
87+
* @return 페이징된 상품 목록
88+
*/
5789
@GetMapping
5890
@Transactional(readOnly = true)
5991
public RsData<PageDto<ProductListItemDto>> getProducts(
@@ -73,6 +105,12 @@ public RsData<PageDto<ProductListItemDto>> getProducts(
73105
return RsData.ok("상품 목록이 조회되었습니다", response);
74106
}
75107

108+
/**
109+
* 상품 목록 조회 (Elasticsearch 기반)
110+
* - Elasticsearch의 전문 검색 기능 활용
111+
* - 한글 형태소 분석(nori analyzer) 지원
112+
* - RDB보다 빠른 검색 성능 제공
113+
*/
76114
@GetMapping("/es")
77115
@Transactional(readOnly = true)
78116
public RsData<PageDto<ProductListItemDto>> getProductsByElasticsearch(
@@ -92,6 +130,14 @@ public RsData<PageDto<ProductListItemDto>> getProductsByElasticsearch(
92130
return RsData.ok("상품 목록이 조회되었습니다", response);
93131
}
94132

133+
/**
134+
* 상품 상세 조회
135+
* - 특정 상품의 모든 정보를 조회
136+
* - 이미지 목록, 판매자 정보 포함
137+
*
138+
* @param productId 조회할 상품의 ID
139+
* @return 상품 상세 정보
140+
*/
95141
@GetMapping("/{productId}")
96142
@Transactional(readOnly = true)
97143
public RsData<ProductResponse> getProduct(@PathVariable Long productId) {
@@ -101,6 +147,19 @@ public RsData<ProductResponse> getProduct(@PathVariable Long productId) {
101147
return RsData.ok("상품이 조회되었습니다", response);
102148
}
103149

150+
/**
151+
* 상품 수정
152+
* - 상품 정보 수정 및 이미지 추가/삭제
153+
* - 경매 시작 전에만 수정 가능
154+
* - 본인의 상품만 수정 가능
155+
*
156+
* @param productId 수정할 상품의 ID
157+
* @param request 수정할 상품 정보 (변경할 필드만 포함)
158+
* @param images 추가할 이미지 파일 (선택)
159+
* @param deleteImageIds 삭제할 이미지 ID 리스트 (선택)
160+
* @param user 현재 로그인한 사용자
161+
* @return 수정된 상품 정보
162+
*/
104163
@PutMapping(value = "/{productId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
105164
@Transactional
106165
public RsData<ProductResponse> modifyProduct(
@@ -121,6 +180,15 @@ public RsData<ProductResponse> modifyProduct(
121180
return RsData.ok("상품이 수정되었습니다", response);
122181
}
123182

183+
/**
184+
* 상품 삭제
185+
* - 경매 시작 전에만 삭제 가능
186+
* - 본인의 상품만 삭제 가능
187+
* - 관련된 모든 이미지 파일도 함께 삭제됨
188+
*
189+
* @param productId 삭제할 상품의 ID
190+
* @param user 현재 로그인한 사용자
191+
*/
124192
@DeleteMapping("/{productId}")
125193
@Transactional
126194
public RsData<Void> deleteProduct(
@@ -137,6 +205,14 @@ public RsData<Void> deleteProduct(
137205
return RsData.ok("상품이 삭제되었습니다");
138206
}
139207

208+
/**
209+
* 내 상품 목록 조회 (RDB 기반)
210+
* - 로그인한 사용자가 등록한 상품 목록 조회
211+
* - 판매 상태별 필터링 가능
212+
* - 낙찰자 및 리뷰 정보 포함
213+
*
214+
* @param status 판매 상태 (SELLING, SOLD, FAILED)
215+
*/
140216
@GetMapping("/me")
141217
@Transactional(readOnly = true)
142218
public RsData<PageDto<MyProductListItemDto>> getMyProducts(
@@ -153,6 +229,11 @@ public RsData<PageDto<MyProductListItemDto>> getMyProducts(
153229
return RsData.ok("내 상품 목록이 조회되었습니다", response);
154230
}
155231

232+
/**
233+
* 내 상품 목록 조회 (Elasticsearch 기반)
234+
* - Elasticsearch를 활용한 빠른 조회
235+
* - 낙찰자 및 리뷰 정보는 RDB에서 별도 조회
236+
*/
156237
@GetMapping("/es/me")
157238
@Transactional(readOnly = true)
158239
public RsData<PageDto<MyProductListItemDto>> getMyProductsByElasticsearch(
@@ -169,6 +250,15 @@ public RsData<PageDto<MyProductListItemDto>> getMyProductsByElasticsearch(
169250
return RsData.ok("내 상품 목록이 조회되었습니다", response);
170251
}
171252

253+
/**
254+
* 특정 회원의 상품 목록 조회 (RDB 기반)
255+
* - 다른 회원이 등록한 상품 목록 조회
256+
* - 판매 상태별 필터링 가능
257+
* - 리뷰 정보 포함
258+
* - 회원 프로필 페이지 등에서 사용
259+
*
260+
* @param memberId 조회할 회원의 ID
261+
*/
172262
@GetMapping("/members/{memberId}")
173263
@Transactional(readOnly = true)
174264
public RsData<PageDto<ProductListByMemberItemDto>> getProductsByMember(
@@ -186,6 +276,11 @@ public RsData<PageDto<ProductListByMemberItemDto>> getProductsByMember(
186276
return RsData.ok("%d번 회원 상품 목록이 조회되었습니다".formatted(memberId), response);
187277
}
188278

279+
/**
280+
* 특정 회원의 상품 목록 조회 (Elasticsearch 기반)
281+
* - Elasticsearch를 활용한 빠른 조회
282+
* - 리뷰 정보는 RDB에서 별도 조회
283+
*/
189284
@GetMapping("/es/members/{memberId}")
190285
@Transactional(readOnly = true)
191286
public RsData<PageDto<ProductListByMemberItemDto>> getProductsByMemberAndElasticsearch(

src/main/java/com/backend/domain/product/document/ProductDocument.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,34 @@
1010

1111
import java.time.LocalDateTime;
1212

13+
/**
14+
* Elasticsearch 상품 문서
15+
* - RDB의 Product 엔티티를 검색용으로 최적화한 문서 구조
16+
* - 전문 검색, 복잡한 필터링, 빠른 정렬에 사용
17+
* - RDB와 실시간 동기화 (이벤트 기반)
18+
*
19+
* 인덱스 구성:
20+
* - 인덱스명: products
21+
* - 설정 파일: elasticsearch/product-settings.json (nori analyzer 설정)
22+
* - 매핑 파일: elasticsearch/product-mappings.json (필드 타입 정의)
23+
*
24+
* 한글 검색 최적화:
25+
* - nori_analyzer: 한글 형태소 분석기
26+
* - 품사 태그 필터링으로 불필요한 조사/어미 제거
27+
* - mixed 모드: 복합명사 분해 및 원형 모두 인덱싱
28+
*
29+
* 주요 검색 필드:
30+
* - productName: 상품명 (text)
31+
* - location: 거래 지역 (text)
32+
* - category: 카테고리 (keyword, exact match)
33+
* - status: 경매 상태 (keyword, exact match)
34+
*
35+
* 정렬 필드:
36+
* - createDate: 최신순
37+
* - currentPrice: 가격순
38+
* - endTime: 마감 임박순
39+
* - bidderCount: 인기순
40+
*/
1341
@Document(indexName = "products")
1442
@Setting(settingPath = "elasticsearch/product-settings.json")
1543
@Mapping(mappingPath = "elasticsearch/product-mappings.json")
@@ -71,8 +99,20 @@ public class ProductDocument {
7199

72100
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
73101
private LocalDateTime createDate;
74-
75-
// Entity -> Document 변환
102+
103+
/**
104+
* Entity → Document 변환 (정적 팩토리 메서드)
105+
* - RDB에서 조회한 Product를 Elasticsearch 문서로 변환
106+
* - 인덱싱 및 재인덱싱 시 사용
107+
*
108+
* 변환 규칙:
109+
* - Enum은 name()으로 변환 (category, deliveryMethod)
110+
* - 연관 엔티티는 ID와 주요 필드만 추출 (seller)
111+
* - 컬렉션은 제외 (bids, productImages)
112+
*
113+
* @param product 변환할 상품 엔티티
114+
* @return Elasticsearch에 저장할 문서
115+
*/
76116
public static ProductDocument fromEntity(Product product) {
77117
return ProductDocument.builder()
78118
.id(String.valueOf(product.getId()))

src/main/java/com/backend/domain/product/entity/Product.java

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -115,28 +115,13 @@ public Product(String productName, String description, ProductCategory category,
115115
}
116116
}
117117

118-
119-
public void addProductImage(ProductImage productImage) {
120-
productImages.add(productImage);
121-
122-
if (thumbnailUrl == null) {
123-
this.thumbnailUrl = productImage.getImageUrl();
124-
}
125-
}
126-
127-
public String getThumbnail() {
128-
if (thumbnailUrl != null) {
129-
return thumbnailUrl;
130-
}
131-
132-
thumbnailUrl = productImages.stream()
133-
.findFirst()
134-
.map(ProductImage::getImageUrl)
135-
.orElse(null);
136-
137-
return thumbnailUrl;
138-
}
139-
118+
/**
119+
* 상품 정보 수정
120+
* - null이 아닌 필드만 업데이트
121+
* - 경매 시작 전에만 수정 가능 (호출 전에 검증 필요)
122+
*
123+
* @param validatedRequest 검증된 수정 요청 (변경할 필드만 non-null)
124+
*/
140125
public void modify(ProductModifyRequest validatedRequest) {
141126
if (validatedRequest.name() != null) this.productName = validatedRequest.name();
142127
if (validatedRequest.description() != null) this.description = validatedRequest.description();
@@ -148,31 +133,52 @@ public void modify(ProductModifyRequest validatedRequest) {
148133
if (validatedRequest.location() != null) this.location = validatedRequest.location();
149134
}
150135

136+
// ======================================= image methods ======================================= //
137+
public void addProductImage(ProductImage productImage) {
138+
productImages.add(productImage);
139+
140+
// 첫 번째 이미지는 썸네일로 자동 설정
141+
if (thumbnailUrl == null) {
142+
this.thumbnailUrl = productImage.getImageUrl();
143+
}
144+
}
145+
151146
public void deleteProductImage(ProductImage productImage) {
152147
productImages.remove(productImage);
153148

149+
// 삭제된 이미지가 썸네일이면 null로 설정
154150
if (thumbnailUrl.equals(productImage.getImageUrl())) {
155151
thumbnailUrl = null;
156152
}
157153
}
158154

159-
public void checkActorCanModify(Member actor) {
160-
if (!actor.equals(seller)) {
161-
throw ProductException.accessModifyForbidden();
155+
public String getThumbnail() {
156+
if (thumbnailUrl != null) {
157+
return thumbnailUrl;
162158
}
163-
}
164159

165-
public void checkActorCanDelete(Member actor) {
166-
if (!actor.equals(seller)) {
167-
throw ProductException.accessDeleteForbidden();
168-
}
160+
// 썸네일이 없으면 첫 번째 이미지 찾아서 설정
161+
thumbnailUrl = productImages.stream()
162+
.findFirst()
163+
.map(ProductImage::getImageUrl)
164+
.orElse(null);
165+
166+
return thumbnailUrl;
169167
}
170168

169+
// ======================================= bid methods ======================================= //
170+
/**
171+
* 낙찰자 조회
172+
* - 경매가 종료되고 낙찰 상태일 때만 반환
173+
* - 현재 최고가와 동일한 입찰가를 가진 입찰자
174+
*/
171175
public Member getBidder() {
176+
// 낙찰 상태가 아니거나 아직 종료되지 않았으면 null 반환
172177
if (!status.equals(AuctionStatus.SUCCESSFUL.getDisplayName()) || endTime.isAfter(LocalDateTime.now())) {
173178
return null;
174179
}
175180

181+
// 현재 최고가와 동일한 입찰을 찾아서 입찰자 반환
176182
return bids.stream()
177183
// .max(Comparator.comparing(Bid::getBidPrice))
178184
.filter(bid -> bid.getBidPrice().equals(currentPrice))
@@ -181,20 +187,48 @@ public Member getBidder() {
181187
.orElse(null);
182188
}
183189

190+
/**
191+
* 입찰 추가 및 입찰자 수 업데이트
192+
* - 양방향 관계 설정
193+
* - 고유 입찰자 수 자동 계산 (동일 회원의 중복 입찰 제거)
194+
*/
184195
public void addBid(Bid bid) {
185196
bids.add(bid);
186197

198+
// 중복 제거한 입찰자 수 계산
187199
int _bidderCount = (int) bids.stream()
188200
.map(b -> b.getMember().getId())
189201
.distinct()
190202
.count();
191203

204+
// 입찰자 수가 변경된 경우에만 업데이트
192205
if (_bidderCount != bidderCount) {
193206
bidderCount = _bidderCount;
194207
}
195208
}
196209

197-
// 테스트 전용 (프로덕션에서는 사용 금지)
210+
// ======================================= auth methods ======================================= //
211+
// 상품 수정 권한 검증 (판매자 본인만 수정 가능)
212+
public void checkActorCanModify(Member actor) {
213+
if (!actor.equals(seller)) {
214+
throw ProductException.accessModifyForbidden();
215+
}
216+
}
217+
218+
// 상품 삭제 권한 검증 (판매자 본인만 삭제 가능)
219+
public void checkActorCanDelete(Member actor) {
220+
if (!actor.equals(seller)) {
221+
throw ProductException.accessDeleteForbidden();
222+
}
223+
}
224+
225+
// ======================================= other methods ======================================= //
226+
/**
227+
* 테스트 전용 빌더
228+
* - 프로덕션 코드에서는 사용 금지
229+
* - ID를 포함한 모든 필드를 직접 설정 가능
230+
* - 단위 테스트에서 목 데이터 생성용
231+
*/
198232
@Builder(builderMethodName = "testBuilder", buildMethodName = "testBuild")
199233
private Product(
200234
Long id, String productName, String description, ProductCategory category,

0 commit comments

Comments
 (0)