# Conflicts:
#	review/src/main/java/com/ktds/hi/review/infra/gateway/ExternalReviewEventHubAdapter.java
This commit is contained in:
정유빈 2025-06-18 10:45:17 +09:00
commit 8fa43a82c9
8 changed files with 571 additions and 532 deletions

View File

@ -522,8 +522,6 @@ public class AnalyticsService implements AnalyticsUseCase {
// 1. 리뷰 데이터 수집
List<String> reviewData = externalReviewPort.getRecentReviews(storeId, days);
log.info("review Data check ===> {}", reviewData);
if (reviewData.isEmpty()) {
log.warn("AI 피드백 생성을 위한 리뷰 데이터가 없습니다: storeId={}", storeId);
return createDefaultAIFeedback(storeId);
@ -533,6 +531,7 @@ public class AnalyticsService implements AnalyticsUseCase {
AiFeedback aiFeedback = aiServicePort.generateFeedback(reviewData);
// 3. 도메인 객체 속성 설정
AiFeedback completeAiFeedback = AiFeedback.builder()
.storeId(storeId)

View File

@ -188,7 +188,7 @@ public class AIServiceAdapter implements AIServicePort {
분석 다음 사항을 고려해주세요:
1. 긍정적 요소는 고객들이 자주 언급하는 좋은 점들
2. 부정적 요소는 고객들이 자주 언급하는 안좋은 점들(없는 경우에는 없음으로 표시)
2. 부정적 요소는 고객들이 자주 언급하는 안좋은 점들
2. 개선점은 부정적 피드백이나 불만사항
3. 추천사항은 매장 운영에 도움이 구체적인 제안
4. 신뢰도 점수는 0.0-1.0 사이의

View File

@ -88,6 +88,10 @@ public class ExternalReviewAdapter implements ExternalReviewPort {
.filter(review -> review.getCreatedAt() != null && review.getCreatedAt().isAfter(cutoffDate))
.map(ReviewListResponse::getContent)
.filter(content -> content != null && !content.trim().isEmpty())
.map(content -> content.replace("`", "")
.replace("\n", "")
.replace("\\", "")
.replace("\"", ""))
.collect(Collectors.toList());

View File

@ -80,7 +80,7 @@ public class ReviewInteractor implements CreateReviewUseCase, DeleteReviewUseCas
@Transactional(readOnly = true)
public List<ReviewListResponse> getStoreReviews(Long storeId, Integer page, Integer size) {
Pageable pageable = PageRequest.of(page != null ? page : 0, size != null ? size : 20);
Page<Review> reviews = reviewRepository.findReviewsByStoreId(storeId, pageable);
Page<Review> reviews = reviewRepository.findReviewsByStoreIdOrderByCreatedAtDesc(storeId, pageable);
return reviews.stream()
.filter(review -> review.getStatus() == ReviewStatus.ACTIVE)

View File

@ -28,6 +28,12 @@ public interface ReviewRepository {
*/
Page<Review> findReviewsByStoreId(Long storeId, Pageable pageable);
/**
* 매장 ID로 리뷰 목록 조회
*/
Page<Review> findReviewsByStoreIdOrderByCreatedAtDesc(Long storeId, Pageable pageable);
/**
* 회원 ID로 리뷰 목록 조회
*/

View File

@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.ktds.hi.review.biz.domain.Review;
import com.ktds.hi.review.biz.domain.ReviewStatus;
import com.ktds.hi.review.biz.usecase.out.ReviewRepository;
import com.ktds.hi.review.infra.gateway.repository.ReviewJpaRepository;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
@ -34,6 +35,7 @@ public class ExternalReviewEventHubAdapter {
@Qualifier("externalReviewEventConsumer")
private final EventHubConsumerClient externalReviewEventConsumer;
private final ReviewJpaRepository reviewJpaRepository;
private final Set<String> processedEventIds = new HashSet<>();
@ -153,10 +155,21 @@ public class ExternalReviewEventHubAdapter {
String platform = (String) event.get("platform");
Integer syncedCount = (Integer) event.get("syncedCount");
// Store에서 발행하는 reviews 배열 처리
@SuppressWarnings("unchecked")
List<Map<String, Object>> reviews = (List<Map<String, Object>>) event.get("reviews");
if (reviews != null) {
for (int i = 0; i < reviews.size(); i++) {
Map<String, Object> review = reviews.get(i);
log.info("Review[{}]: {}", i, review);
}
} else {
log.info("No reviews found in event.");
}
if (reviews == null || reviews.isEmpty()) {
log.warn("리뷰 데이터가 없습니다: platform={}, storeId={}", platform, storeId);
return;
@ -191,16 +204,22 @@ public class ExternalReviewEventHubAdapter {
*/
private Review saveExternalReview(Long storeId, String platform, Map<String, Object> reviewData) {
try {
String nickname = createMemberNickname(platform, reviewData);
// 단순화된 매핑
if (reviewJpaRepository.existsByStoreIdAndExternalNickname(storeId, nickname)) {
log.info("중복 리뷰 스킵: storeId={}, nickname={}", storeId, nickname);
return null;
}
Review review = Review.builder()
.storeId(storeId)
.memberId(null) // 외부 리뷰는 회원 ID 없음
.memberId(-1L)
.memberNickname(createMemberNickname(platform, reviewData))
.rating(extractRating(reviewData))
.content(extractContent(reviewData))
.imageUrls(new ArrayList<>()) // 외부 리뷰는 이미지 없음
.status(ReviewStatus.ACTIVE)
.likeCount(0) // 고정값 0
.likeCount(0)
.dislikeCount(0)
.build();

View File

@ -41,6 +41,13 @@ public class ReviewRepositoryAdapter implements ReviewRepository {
return entities.map(this::toDomain);
}
@Override
public Page<Review> findReviewsByStoreIdOrderByCreatedAtDesc(Long storeId, Pageable pageable) {
Page<ReviewEntity> entities = reviewJpaRepository.findByStoreIdAndStatus(storeId, ReviewStatus.ACTIVE,
pageable);
return entities.map(this::toDomain);
}
@Override
public Page<Review> findReviewsByMemberId(Long memberId, Pageable pageable) {
Page<ReviewEntity> entities = reviewJpaRepository.findByMemberIdAndStatus(memberId, ReviewStatus.ACTIVE, pageable);

View File

@ -5,6 +5,8 @@ import com.ktds.hi.review.infra.gateway.entity.ReviewEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@ -31,8 +33,10 @@ public interface ReviewJpaRepository extends JpaRepository<ReviewEntity, Long> {
*/
Optional<ReviewEntity> findByIdAndMemberId(Long id, Long memberId);
/**
* 매장 ID와 회원 ID로 리뷰 존재 여부 확인
* 닉네임으로 외부 리뷰 중복 체크
*/
boolean existsByStoreIdAndMemberId(Long storeId, Long memberId);
@Query("SELECT COUNT(r) > 0 FROM ReviewEntity r WHERE r.storeId = :storeId AND r.memberId = -1 AND r.memberNickname = :nickname")
boolean existsByStoreIdAndExternalNickname(@Param("storeId") Long storeId, @Param("nickname") String nickname);
}