Skip to content

Commit 41db080

Browse files
authored
Refactir/354 대시보드 찜기능 구현 (#366)
* refactor/336 입금 환전 내역 수정 * refactor/336 입금 환전 내역 수정 * refactor/336 작가 수익 테스트 수정 * refactor/336 대시보드 메인현황 팔로우수 추가 * refactor/336 대시보드 메인현황 팔로우수 테스트 케이스 작성 * refactor/336 대시보드 팔로우 작가 조회 * refactor/336 대시보드 팔로우 작가 조회 * refactor/336 Response 수정 * refactor/354리뷰 mock 제거 실제 db연동 * refactor/354 레파지토리에 찜 기능 조회 추가 * refactor/354 찜 테스트 기능 추가 및 테스트 오류 수정 * refactor/354 찜 기능 수정 및 팔로우 기능 수정 * refactor/354 찜 기능 수정 및 팔로우 기능 수정 * refactor/354 찜 기능 수정 및 팔로우 기능 수정
1 parent 7a8e9c6 commit 41db080

File tree

8 files changed

+504
-189
lines changed

8 files changed

+504
-189
lines changed

src/main/java/com/back/domain/dashboard/customer/controller/DashboardController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
/**
2020
* 고객용 대시보드 컨트롤러
2121
* 고객이 자신의 계정 정보, 주문 내역, 작가 신청 현황 등을 조회할 수 있는 대시보드 기능을 제공
22-
* 모든 API는 JWT 인증이 필요
22+
* 모든 API는 JWT 인증이 필요.
2323
* 제공 기능:
2424
* <ul>
2525
* <li>계정 설정 조회 (프로필, 연락처, 보안)</li>

src/main/java/com/back/domain/dashboard/customer/dto/response/WishlistResponse.java

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,85 +6,93 @@
66

77
/**
88
* 찜하기 관련 응답 DTO
9-
*사용자가 찜한 상품들의 정보를 포함
10-
*2025.09.22 수정
9+
* 사용자가 찜한 상품들의 정보를 포함
10+
* 2025.09.22 수정
1111
*/
1212
public class WishlistResponse {
13-
13+
1414
/**
1515
* 찜한 상품 목록 응답
1616
*/
1717
public static class List extends PageResponse<WishlistResponse.Item> {
18-
/** 찜하기 현황 요약 정보 */
18+
/**
19+
* 찜하기 현황 요약 정보
20+
*/
1921
private final SummaryDto summary;
20-
/** 일괄 작업 옵션 */
22+
/**
23+
* 일괄 작업 옵션
24+
*/
2125
private final java.util.List<BulkAction> bulkActions;
22-
26+
2327
public List() {
2428
super();
2529
this.summary = null;
2630
this.bulkActions = null;
2731
}
28-
29-
public List(SummaryDto summary, java.util.List<BulkAction> bulkActions,
30-
java.util.List<Item> content, int page, int size,
31-
long totalElements, int totalPages, boolean hasNext, boolean hasPrevious) {
32+
33+
public List(SummaryDto summary, java.util.List<BulkAction> bulkActions,
34+
java.util.List<Item> content, int page, int size,
35+
long totalElements, int totalPages, boolean hasNext, boolean hasPrevious) {
3236
super(content, page, size, totalElements, totalPages, hasNext, hasPrevious);
3337
this.summary = summary;
3438
this.bulkActions = bulkActions;
3539
}
36-
40+
3741
public SummaryDto getSummary() {
3842
return summary;
3943
}
40-
44+
4145
public java.util.List<BulkAction> getBulkActions() {
4246
return bulkActions;
4347
}
4448
}
45-
49+
4650
/**
4751
* 찜하기 현황 요약 정보
4852
*/
4953
public record SummaryDto(
5054
/** 전체 찜한 상품 수 */
5155
int totalWishItems
52-
) {}
53-
56+
) {
57+
}
58+
5459
/**
5560
* 찜한 상품 정보
5661
*/
5762
public record Item(
5863
String wishId,
5964
Long productId,
6065
String productNumber,
66+
String brandName,
6167
String productName,
6268
int price,
6369
Artist artist,
6470
String imageUrl,
6571
String sellingStatus,
66-
String registeredDate,
6772
LocalDateTime addedAt,
6873
String productPageUrl,
6974
Permission permissions
70-
) {}
71-
75+
) {
76+
}
77+
7278
/**
7379
* 작가 정보
7480
*/
7581
public record Artist(
7682
String id,
7783
String name
78-
) {}
79-
84+
) {
85+
}
86+
8087
/**
8188
* 권한 정보
8289
*/
8390
public record Permission(
8491
/** 찜 해제 가능 여부 */
8592
Boolean canUnwish
86-
) {}
87-
93+
) {
94+
}
95+
8896
/**
8997
* 일괄 작업 옵션
9098
*/
@@ -95,5 +103,6 @@ public record BulkAction(
95103
String label,
96104
/** 확인 필요 여부 */
97105
Boolean requiresConfirmation
98-
) {}
106+
) {
107+
}
99108
}

src/main/java/com/back/domain/dashboard/customer/service/DashboardServiceImpl.java

Lines changed: 102 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class DashboardServiceImpl implements DashboardService {
4444
private final com.back.domain.payment.moriCash.repository.MoriCashPaymentRepository moriCashPaymentRepository;
4545
private final com.back.domain.payment.moriCash.repository.MoriCashBalanceRepository moriCashBalanceRepository;
4646
private final com.back.domain.follow.repository.FollowRepository followRepository;
47+
private final com.back.domain.wishlist.repository.WishlistRepository wishlistRepository;
4748

4849
private static final DateTimeFormatter ORDER_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy. MM. dd");
4950
private static final DateTimeFormatter FUNDING_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy. MM. dd");
@@ -119,7 +120,7 @@ public ArtistApplicationResponse.List getArtistApplications(Long userId, ArtistA
119120
int start = request.page() * request.size();
120121
int end = Math.min(start + request.size(), applications.size());
121122
List<com.back.domain.artist.entity.ArtistApplication> pagedApplications =
122-
applications.subList(start, Math.min(end, applications.size()));
123+
applications.subList(start, end);
123124

124125
// 5. DTO 변환
125126
List<ArtistApplicationResponse.Summary> content = pagedApplications.stream()
@@ -408,15 +409,23 @@ private String getProductThumbnailUrl(com.back.domain.product.product.entity.Pro
408409
return null;
409410
}
410411

411-
if (product.getImages() == null || product.getImages().isEmpty()) {
412+
try {
413+
if (product.getImages() == null || product.getImages().isEmpty()) {
414+
return null;
415+
}
416+
417+
return product.getImages().stream()
418+
.filter(image -> image != null &&
419+
image.getFileType() != null &&
420+
"THUMBNAIL".equals(image.getFileType().name()))
421+
.findFirst()
422+
.map(com.back.domain.product.product.entity.ProductImage::getFileUrl)
423+
.orElse(null);
424+
} catch (Exception e) {
425+
// LazyInitializationException 등의 에러 발생 시 null 반환
426+
log.warn("상품 이미지 컬렉션 접근 실패 - productId: {}", product.getId(), e);
412427
return null;
413428
}
414-
415-
return product.getImages().stream()
416-
.filter(image -> "THUMBNAIL".equals(image.getFileType().name()))
417-
.findFirst()
418-
.map(com.back.domain.product.product.entity.ProductImage::getFileUrl)
419-
.orElse(null);
420429
}
421430

422431
/**
@@ -451,83 +460,127 @@ private String mapOrderStatusText(com.back.domain.order.order.entity.OrderStatus
451460
public FollowingResponse.List getFollowingArtists(Long userId, FollowingSearchRequest request) {
452461
log.debug("팔로우한 작가 목록 조회 - userId: {}, request: {}", userId, request);
453462

454-
// 1. 사용자 조회
455-
User user = userRepository.findById(userId)
456-
.orElseThrow(() -> new ServiceException("USER_NOT_FOUND", "사용자를 찾을 수 없습니다."));
463+
// 1. 사용자 존재 여부 확인
464+
if (!userRepository.existsById(userId)) {
465+
throw new ServiceException("USER_NOT_FOUND", "사용자를 찾을 수 없습니다.");
466+
}
457467

458468
// 2. 팔로우 목록 조회
459469
List<com.back.domain.follow.entity.Follow> follows =
460470
followRepository.findFollowingsByFollowerId(userId);
461471

462472
// 3. 페이징 처리
463-
long total = follows.size();
464473
int start = request.page() * request.size();
465474
int end = Math.min(start + request.size(), follows.size());
466475
List<com.back.domain.follow.entity.Follow> pagedFollows =
467-
follows.subList(start, Math.min(end, follows.size()));
476+
start < follows.size() ? follows.subList(start, end) : List.of();
468477

469478
// 4. DTO 변환
470479
List<FollowingResponse.Artist> content = pagedFollows.stream()
471480
.map(this::convertToFollowingArtist)
472481
.collect(Collectors.toList());
473482

474483
// 5. 페이징 정보 계산
475-
int totalPages = (int) Math.ceil((double) total / request.size());
476-
boolean hasNext = request.page() < totalPages - 1;
477-
boolean hasPrevious = request.page() > 0;
484+
int totalPages = (int) Math.ceil((double) follows.size() / request.size());
478485

479486
return new FollowingResponse.List(
480487
content,
481488
request.page(), request.size(),
482-
total, totalPages, hasNext, hasPrevious);
489+
follows.size(), totalPages,
490+
end < follows.size(),
491+
request.page() > 0
492+
);
483493
}
484494

485495
/**
486496
* Follow 엔티티를 Artist DTO로 변환
487497
*/
488498
private FollowingResponse.Artist convertToFollowingArtist(com.back.domain.follow.entity.Follow follow) {
489-
com.back.domain.artist.entity.ArtistProfile artist = follow.getFollowingArtist();
499+
com.back.domain.artist.entity.ArtistProfile artistProfile = follow.getFollowingArtist();
490500

491501
return new FollowingResponse.Artist(
492-
artist.getId().toString(),
493-
artist.getArtistName(),
494-
artist.getProfileImageUrl(), // null인 경우 프론트에서 기본 이미지 처리
495-
"/artists/" + artist.getId()
502+
artistProfile.getId().toString(),
503+
artistProfile.getArtistName(),
504+
artistProfile.getProfileImageUrl(),
505+
"/artists/" + artistProfile.getId()
496506
);
497507
}
498508

499509
@Override
500510
public WishlistResponse.List getWishlist(Long userId, WishlistSearchRequest request) {
501-
// TODO: 실제 데이터베이스 조회 로직 구현
502511
log.debug("찜한 상품 목록 조회 - userId: {}, request: {}", userId, request);
503512

504-
WishlistResponse.SummaryDto summary = new WishlistResponse.SummaryDto(15);
513+
// 1. 사용자 존재 여부 확인
514+
if (!userRepository.existsById(userId)) {
515+
throw new ServiceException("USER_NOT_FOUND", "사용자를 찾을 수 없습니다.");
516+
}
517+
518+
// 2. 페이징 설정 (페이지 크기 8개 고정)
519+
int fixedPageSize = 8;
520+
Pageable pageable = PageRequest.of(request.page(), fixedPageSize);
521+
522+
// 3. 찜 목록 조회 (Product, Artist 정보 포함, 삭제된 상품 제외)
523+
Page<com.back.domain.wishlist.entity.Wishlist> wishlistPage =
524+
wishlistRepository.findByUserIdWithProductAndArtist(userId, pageable);
525+
526+
// 4. DTO 변환
527+
List<WishlistResponse.Item> content = wishlistPage.getContent().stream()
528+
.map(this::convertToWishlistItem)
529+
.collect(Collectors.toList());
530+
531+
// 5. 통계 계산 (삭제되지 않은 상품만 카운트)
532+
long totalWishItems = wishlistPage.getTotalElements();
533+
WishlistResponse.SummaryDto summary = new WishlistResponse.SummaryDto((int) totalWishItems);
505534

535+
// 6. 일괄 작업 옵션
506536
List<WishlistResponse.BulkAction> bulkActions = List.of(
507537
new WishlistResponse.BulkAction("BULK_UNWISH", "선택 항목 해제", true)
508538
);
509539

510-
List<WishlistResponse.Item> content = List.of(
511-
new WishlistResponse.Item(
512-
"w-001", 123157L, "0123157", "감성 일러스트 포스터", 25000,
513-
new WishlistResponse.Artist("artist001", "감성작가"),
514-
"https://cdn.example.com/p/123157/main.jpg", "SELLING", "2025-09-18",
515-
LocalDateTime.now(), "/products/0123157",
516-
new WishlistResponse.Permission(true)
517-
),
518-
new WishlistResponse.Item(
519-
"w-002", 123158L, "0123158", "귀여운 스티커 세트", 15000,
520-
new WishlistResponse.Artist("artist002", "캐릭터작가"),
521-
"https://cdn.example.com/p/123158/main.jpg", "SELLING", "2025-09-17",
522-
LocalDateTime.now().minusDays(1), "/products/0123158",
523-
new WishlistResponse.Permission(true)
524-
)
525-
);
526-
527540
return new WishlistResponse.List(
528541
summary, bulkActions, content,
529-
request.page(), request.size(),
530-
15, 2, true, false);
542+
wishlistPage.getNumber(), fixedPageSize,
543+
wishlistPage.getTotalElements(), wishlistPage.getTotalPages(),
544+
wishlistPage.hasNext(), wishlistPage.hasPrevious()
545+
);
546+
}
547+
548+
/**
549+
* Wishlist 엔티티를 Item DTO로 변환
550+
*/
551+
private WishlistResponse.Item convertToWishlistItem(com.back.domain.wishlist.entity.Wishlist wishlist) {
552+
com.back.domain.product.product.entity.Product product = wishlist.getProduct();
553+
554+
// 작가 정보 (Product의 user가 작가)
555+
WishlistResponse.Artist artist = null;
556+
if (product.getUser() != null) {
557+
artist = new WishlistResponse.Artist(
558+
product.getUser().getId().toString(),
559+
product.getUser().getName()
560+
);
561+
}
562+
563+
// 상품 상태 매핑
564+
String sellingStatus = product.getSellingStatus() != null ?
565+
product.getSellingStatus().name() : "UNKNOWN";
566+
567+
// 썸네일 이미지 URL
568+
String imageUrl = getProductThumbnailUrl(product);
569+
570+
return new WishlistResponse.Item(
571+
"w-" + wishlist.getId(),
572+
product.getId(),
573+
String.format("%07d", product.getId()),
574+
product.getBrandName() != null ? product.getBrandName() : "",
575+
product.getName(),
576+
product.getPrice(),
577+
artist,
578+
imageUrl,
579+
sellingStatus,
580+
wishlist.getCreateDate(),
581+
"/products/" + product.getId(),
582+
new WishlistResponse.Permission(true)
583+
);
531584
}
532585

533586
@Override
@@ -626,7 +679,9 @@ private String getFundingThumbnailUrl(Funding funding) {
626679
// images 컬렉션에서 THUMBNAIL 타입 찾기
627680
if (funding.getImages() != null && !funding.getImages().isEmpty()) {
628681
String thumbnailUrl = funding.getImages().stream()
629-
.filter(image -> image != null && "THUMBNAIL".equals(image.getFileType().name()))
682+
.filter(image -> image != null &&
683+
image.getFileType() != null &&
684+
"THUMBNAIL".equals(image.getFileType().name()))
630685
.findFirst()
631686
.map(FundingImage::getFileUrl)
632687
.orElse(null);
@@ -729,8 +784,8 @@ public CashResponse.HistoryList getCashHistory(Long userId, CashHistorySearchReq
729784
tx.getBalanceAfter() != null ? tx.getBalanceAfter() : 0,
730785
"모리캐시",
731786
"COMPLETED",
732-
tx.getOrder() != null ?
733-
new CashResponse.Link("/orders/" + tx.getOrder().getOrderNumber()) : null
787+
tx.getOrder() != null ?
788+
new CashResponse.Link("/orders/" + tx.getOrder().getOrderNumber()) : null
734789
)));
735790

736791
// 4. 날짜순 정렬 (최신순)
@@ -739,7 +794,7 @@ public CashResponse.HistoryList getCashHistory(Long userId, CashHistorySearchReq
739794
// 5. 페이징 처리
740795
int start = request.page() * request.size();
741796
int end = Math.min(start + request.size(), allTransactions.size());
742-
var pagedContent = start < allTransactions.size() ?
797+
var pagedContent = start < allTransactions.size() ?
743798
allTransactions.subList(start, end) : List.<CashResponse.Transaction>of();
744799

745800
// 6. 통계 계산

src/main/java/com/back/domain/product/product/scheduler/ProductPopularityScheduler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void updatePopularityScores() {
3333
long salesCount = orderItemRepository.countByProduct(product);
3434

3535
// 찜 수 계산 (가중치 20%)
36-
long wishlistCount = wishlistRepository.countByProduct(product);
36+
long wishlistCount = wishlistRepository.countByProductId(product.getId());
3737

3838
// 리뷰 평점 (가중치 20%)
3939
Double averageRating = product.getAverageRating();

0 commit comments

Comments
 (0)