Skip to content

Commit 770896b

Browse files
committed
feat: 마이페이지 맞춤형 배너 알림 조회 API 구현
1 parent f82b257 commit 770896b

File tree

6 files changed

+271
-0
lines changed

6 files changed

+271
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.ongil.backend.domain.banner.controller;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
import com.ongil.backend.domain.banner.dto.response.BannerResponse;
10+
import com.ongil.backend.domain.banner.service.BannerService;
11+
import com.ongil.backend.global.common.dto.DataResponse;
12+
13+
import io.swagger.v3.oas.annotations.Operation;
14+
import io.swagger.v3.oas.annotations.tags.Tag;
15+
import lombok.RequiredArgsConstructor;
16+
17+
@Tag(name = "Banner", description = "배너 알림 관련 API")
18+
@RestController
19+
@RequiredArgsConstructor
20+
@RequestMapping("/api/banner")
21+
public class BannerController {
22+
23+
private final BannerService bannerService;
24+
25+
@GetMapping
26+
@Operation(summary = "배너 조회 API", description = "토큰 필요. 현재 사용자에게 보여줄 배너를 조회합니다. 구매 직후 리뷰 유도, 한달 후기 유도, 매거진 추천 중 우선순위에 따라 반환됩니다.")
27+
public ResponseEntity<DataResponse<BannerResponse>> getBanner(
28+
@AuthenticationPrincipal Long userId
29+
) {
30+
BannerResponse response = bannerService.getBanner(userId);
31+
return ResponseEntity.ok(DataResponse.from(response));
32+
}
33+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.ongil.backend.domain.banner.converter;
2+
3+
import org.springframework.stereotype.Component;
4+
5+
import com.ongil.backend.domain.banner.dto.response.BannerResponse;
6+
import com.ongil.backend.domain.banner.enums.BannerType;
7+
8+
@Component
9+
public class BannerConverter {
10+
11+
public BannerResponse toResponse(BannerType type, String title, String buttonText,
12+
String targetUrl, Long targetId, boolean enabled) {
13+
return BannerResponse.builder()
14+
.type(type)
15+
.title(title)
16+
.buttonText(buttonText)
17+
.targetUrl(targetUrl)
18+
.targetId(targetId)
19+
.enabled(enabled)
20+
.build();
21+
}
22+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.ongil.backend.domain.banner.dto.response;
2+
3+
import com.ongil.backend.domain.banner.enums.BannerType;
4+
5+
import lombok.Builder;
6+
7+
@Builder
8+
public record BannerResponse(
9+
BannerType type,
10+
String title,
11+
String buttonText,
12+
String targetUrl,
13+
Long targetId,
14+
boolean enabled
15+
) {
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.ongil.backend.domain.banner.enums;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
@Getter
7+
@RequiredArgsConstructor
8+
public enum BannerType {
9+
MAGAZINE("매거진 유도"),
10+
REVIEW_PROMPT("구매 직후 리뷰 작성 유도"),
11+
MONTHLY_REVIEW_PROMPT("한달 후 리뷰 작성 유도");
12+
13+
private final String description;
14+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.ongil.backend.domain.banner.service;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.List;
5+
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Transactional;
8+
9+
import com.ongil.backend.domain.banner.converter.BannerConverter;
10+
import com.ongil.backend.domain.banner.dto.response.BannerResponse;
11+
import com.ongil.backend.domain.banner.enums.BannerType;
12+
import com.ongil.backend.domain.order.entity.Order;
13+
import com.ongil.backend.domain.order.entity.OrderItem;
14+
import com.ongil.backend.domain.order.enums.OrderStatus;
15+
import com.ongil.backend.domain.order.repository.OrderRepository;
16+
import com.ongil.backend.domain.review.enums.ReviewType;
17+
import com.ongil.backend.domain.review.repository.ReviewRepository;
18+
19+
import lombok.RequiredArgsConstructor;
20+
21+
@Service
22+
@RequiredArgsConstructor
23+
@Transactional(readOnly = true)
24+
public class BannerService {
25+
26+
private final OrderRepository orderRepository;
27+
private final ReviewRepository reviewRepository;
28+
private final BannerConverter bannerConverter;
29+
30+
public BannerResponse getBanner(Long userId) {
31+
// 1순위: 구매직후 후기 미작성
32+
BannerResponse initialReviewBanner = checkInitialReviewBanner(userId);
33+
if (initialReviewBanner != null) {
34+
return initialReviewBanner;
35+
}
36+
37+
// 2순위: 한달 후 후기 미작성
38+
BannerResponse monthlyReviewBanner = checkMonthlyReviewBanner(userId);
39+
if (monthlyReviewBanner != null) {
40+
return monthlyReviewBanner;
41+
}
42+
43+
// 3순위: 매거진 유도 알림
44+
return createMagazineBanner();
45+
}
46+
47+
private BannerResponse checkInitialReviewBanner(Long userId) {
48+
List<Order> confirmedOrders = orderRepository.findByUserIdAndStatus(
49+
userId,
50+
OrderStatus.CONFIRMED
51+
);
52+
53+
for (Order order : confirmedOrders) {
54+
OrderItem pendingItem = findPendingInitialReviewItem(order);
55+
if (pendingItem != null) {
56+
return bannerConverter.toResponse(
57+
BannerType.REVIEW_PROMPT,
58+
"구매하신 상품은 어떠셨나요?",
59+
"작성하러 가기",
60+
"/review/write",
61+
order.getId(),
62+
true
63+
);
64+
}
65+
}
66+
67+
return null;
68+
}
69+
70+
private BannerResponse checkMonthlyReviewBanner(Long userId) {
71+
LocalDateTime fiveDaysAgo = LocalDateTime.now().minusDays(5);
72+
73+
List<Order> orders = orderRepository.findByUserIdAndStatusAndConfirmedAtBefore(
74+
userId,
75+
OrderStatus.CONFIRMED,
76+
fiveDaysAgo
77+
);
78+
79+
for (Order order : orders) {
80+
OrderItem pendingItem = findPendingMonthlyReviewItem(order);
81+
if (pendingItem != null) {
82+
return bannerConverter.toResponse(
83+
BannerType.MONTHLY_REVIEW_PROMPT,
84+
"한달 후기를 작성해주세요!",
85+
"작성하러 가기",
86+
"/review/monthly/write",
87+
order.getId(),
88+
true
89+
);
90+
}
91+
}
92+
93+
return null;
94+
}
95+
96+
private BannerResponse createMagazineBanner() {
97+
return bannerConverter.toResponse(
98+
BannerType.MAGAZINE,
99+
"추천 매거진을 확인해보세요",
100+
"보러가기",
101+
"/magazine",
102+
null,
103+
true
104+
);
105+
}
106+
107+
private OrderItem findPendingInitialReviewItem(Order order) {
108+
for (OrderItem item : order.getOrderItems()) {
109+
boolean hasInitialReview = reviewRepository.existsByOrderItemIdAndReviewType(
110+
item.getId(),
111+
ReviewType.INITIAL
112+
);
113+
if (!hasInitialReview) {
114+
return item;
115+
}
116+
}
117+
return null;
118+
}
119+
120+
private OrderItem findPendingMonthlyReviewItem(Order order) {
121+
for (OrderItem item : order.getOrderItems()) {
122+
boolean hasMonthlyReview = reviewRepository.existsByOrderItemIdAndReviewType(
123+
item.getId(),
124+
ReviewType.ONE_MONTH
125+
);
126+
if (!hasMonthlyReview) {
127+
return item;
128+
}
129+
}
130+
return null;
131+
}
132+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,62 @@
11
package com.ongil.backend.domain.order.repository;
22

3+
import java.time.LocalDateTime;
4+
import java.util.List;
5+
36
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
49

510
import com.ongil.backend.domain.order.entity.Order;
11+
import com.ongil.backend.domain.order.enums.OrderStatus;
612

713
public interface OrderRepository extends JpaRepository<Order, Long> {
14+
15+
// 특정 사용자의 주문 확정 시간 범위로 주문 조회
16+
@Query("SELECT o FROM Order o " +
17+
"WHERE o.user.id = :userId " +
18+
"AND o.orderStatus = :status " +
19+
"AND o.confirmedAt >= :startTime " +
20+
"AND o.confirmedAt < :endTime " +
21+
"ORDER BY o.confirmedAt DESC")
22+
List<Order> findByUserIdAndStatusAndConfirmedAtBetween(
23+
@Param("userId") Long userId,
24+
@Param("status") OrderStatus status,
25+
@Param("startTime") LocalDateTime startTime,
26+
@Param("endTime") LocalDateTime endTime
27+
);
28+
29+
// 특정 사용자의 주문 확정 시간 이후 주문 조회 (5일 전 이후 확정된 주문)
30+
@Query("SELECT o FROM Order o " +
31+
"WHERE o.user.id = :userId " +
32+
"AND o.orderStatus = :status " +
33+
"AND o.confirmedAt >= :afterTime " +
34+
"ORDER BY o.confirmedAt ASC")
35+
List<Order> findByUserIdAndStatusAndConfirmedAtAfter(
36+
@Param("userId") Long userId,
37+
@Param("status") OrderStatus status,
38+
@Param("afterTime") LocalDateTime afterTime
39+
);
40+
41+
// 특정 사용자의 특정 상태 주문 조회
42+
@Query("SELECT o FROM Order o " +
43+
"WHERE o.user.id = :userId " +
44+
"AND o.orderStatus = :status " +
45+
"ORDER BY o.confirmedAt DESC")
46+
List<Order> findByUserIdAndStatus(
47+
@Param("userId") Long userId,
48+
@Param("status") OrderStatus status
49+
);
50+
51+
// 특정 사용자의 주문 확정 시간 이전 주문 조회 (한달 이상 경과)
52+
@Query("SELECT o FROM Order o " +
53+
"WHERE o.user.id = :userId " +
54+
"AND o.orderStatus = :status " +
55+
"AND o.confirmedAt <= :beforeTime " +
56+
"ORDER BY o.confirmedAt DESC")
57+
List<Order> findByUserIdAndStatusAndConfirmedAtBefore(
58+
@Param("userId") Long userId,
59+
@Param("status") OrderStatus status,
60+
@Param("beforeTime") LocalDateTime beforeTime
61+
);
862
}

0 commit comments

Comments
 (0)