From 23c1d9d2447105b3bcbf50d8fa628554a8848870 Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Wed, 18 Jun 2025 15:31:00 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat=20:=20=EB=B6=84=EC=84=9D=20api=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biz/service/AnalyticsService.java | 121 ++++++++++++++++-- .../infra/dto/ReviewAnalysisResponse.java | 2 + 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java index e1343e2..906c3ae 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java @@ -4,6 +4,7 @@ import com.ktds.hi.analytics.biz.domain.ActionPlan; import com.ktds.hi.analytics.biz.domain.Analytics; import com.ktds.hi.analytics.biz.domain.AiFeedback; import com.ktds.hi.analytics.biz.domain.PlanStatus; +import com.ktds.hi.analytics.biz.domain.SentimentType; import com.ktds.hi.analytics.biz.usecase.in.AnalyticsUseCase; import com.ktds.hi.analytics.biz.usecase.out.*; import com.ktds.hi.analytics.infra.dto.*; @@ -13,10 +14,14 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; /** * 분석 서비스 구현 클래스 (수정버전) @@ -35,6 +40,7 @@ public class AnalyticsService implements AnalyticsUseCase { private final CachePort cachePort; private final EventPort eventPort; private final ActionPlanPort actionPlanPort; // 추가된 의존성 + @Override // @Cacheable(value = "storeAnalytics", key = "#storeId") @@ -280,8 +286,10 @@ public class AnalyticsService implements AnalyticsUseCase { } // 3. 응답 생성 - int positiveCount = countPositiveReviews(recentReviews); - int negativeCount = countNegativeReviews(recentReviews); + ReviewSentimentCount sentimentCount = analyzeReviewSentiments(recentReviews); + int positiveCount = sentimentCount.getPositiveCount(); + int negativeCount = sentimentCount.getNegativeCount(); + int neutralCount = sentimentCount.getNeutralCount(); int totalCount = recentReviews.size(); ReviewAnalysisResponse response = ReviewAnalysisResponse.builder() @@ -289,8 +297,10 @@ public class AnalyticsService implements AnalyticsUseCase { .totalReviews(totalCount) .positiveReviewCount(positiveCount) .negativeReviewCount(negativeCount) - .positiveRate(Math.floor((double) positiveCount / totalCount * 100) / 10.0) - .negativeRate(Math.floor((double) negativeCount / totalCount * 100) / 10.0) + .neutralReviewCount(neutralCount) + .positiveRate(Math.floor((double) positiveCount / totalCount * 1000) / 10.0) + .negativeRate(Math.floor((double) negativeCount / totalCount * 1000) / 10.0) + .neutralRate(Math.floor((double) neutralCount / totalCount * 1000) / 10.0) .analysisDate(LocalDate.now()) .build(); @@ -305,7 +315,99 @@ public class AnalyticsService implements AnalyticsUseCase { throw new RuntimeException("리뷰 분석에 실패했습니다.", e); } } - + + /** + * LLM 기반 리뷰 감정 분석 - 한 번의 분석으로 긍정/부정/중립 수 모두 반환 + * + * @param reviews 분석할 리뷰 목록 + * @return ReviewSentimentCount 감정별 리뷰 수 + */ + private ReviewSentimentCount analyzeReviewSentiments(List reviews) { + log.info("LLM 기반 리뷰 감정 분석 시작: 총 리뷰 수={}", reviews.size()); + + try { + if (reviews.isEmpty()) { + return new ReviewSentimentCount(0, 0, 0); + } + + // 유효한 리뷰만 필터링 + List validReviews = reviews.stream() + .filter(review -> review != null && !review.trim().isEmpty()) + .collect(Collectors.toList()); + + if (validReviews.isEmpty()) { + return new ReviewSentimentCount(0, 0, 0); + } + + int positiveCount = 0; + int negativeCount = 0; + int neutralCount = 0; + + // 각 리뷰를 AI로 감정 분석 + for (String review : validReviews) { + try { + SentimentType sentiment = aiServicePort.analyzeSentiment(review); + + switch (sentiment) { + case POSITIVE: + positiveCount++; + break; + case NEGATIVE: + negativeCount++; + break; + case NEUTRAL: + default: + neutralCount++; + break; + } + + } catch (Exception e) { + log.warn("개별 리뷰 감정 분석 실패, 중립으로 처리: {}", + review.substring(0, Math.min(30, review.length())), e); + neutralCount++; // 분석 실패 시 중립으로 처리 + } + } + + ReviewSentimentCount result = new ReviewSentimentCount(positiveCount, negativeCount, neutralCount); + + log.info("리뷰 감정 분석 완료: 긍정={}, 부정={}, 중립={}, 전체={}", + positiveCount, negativeCount, neutralCount, validReviews.size()); + + return result; + + } catch (Exception e) { + log.error("리뷰 감정 분석 중 전체 오류 발생, fallback 사용", e); + // 오류 시 기존 가정값 사용 + int total = reviews.size(); + return new ReviewSentimentCount( + (int) (total * 0.6), // 60% 긍정 + (int) (total * 0.2), // 20% 부정 + total - (int) (total * 0.6) - (int) (total * 0.2) // 나머지 중립 + ); + } + } + + /** + * 리뷰 감정 분석 결과를 담는 내부 클래스 + */ + public static class ReviewSentimentCount { + private final int positiveCount; + private final int negativeCount; + private final int neutralCount; + + public ReviewSentimentCount(int positiveCount, int negativeCount, int neutralCount) { + this.positiveCount = positiveCount; + this.negativeCount = negativeCount; + this.neutralCount = neutralCount; + } + + public int getPositiveCount() { return positiveCount; } + public int getNegativeCount() { return negativeCount; } + public int getNeutralCount() { return neutralCount; } + public int getTotalCount() { return positiveCount + negativeCount + neutralCount; } + } + + // private 메서드들 @Transactional public Analytics generateNewAnalytics(Long storeId) { @@ -429,15 +531,6 @@ public class AnalyticsService implements AnalyticsUseCase { return "추천사항이 없습니다."; } - private int countPositiveReviews(List reviews) { - // 실제로는 AI 서비스를 통한 감정 분석 필요 - return (int) (reviews.size() * 0.6); // 60% 가정 - } - - private int countNegativeReviews(List reviews) { - // 실제로는 AI 서비스를 통한 감정 분석 필요 - return (int) (reviews.size() * 0.2); // 20% 가정 - } @Override @Transactional diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/ReviewAnalysisResponse.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/ReviewAnalysisResponse.java index c815614..dfc5197 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/ReviewAnalysisResponse.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/ReviewAnalysisResponse.java @@ -20,7 +20,9 @@ public class ReviewAnalysisResponse { private Integer totalReviews; private Integer positiveReviewCount; private Integer negativeReviewCount; + private Integer neutralReviewCount; private Double positiveRate; private Double negativeRate; + private Double neutralRate; private LocalDate analysisDate; } From cc3a0b84c532988291faf28685729bcc7f55a26c Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Wed, 18 Jun 2025 15:44:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat=20:=20=EB=B6=84=EC=84=9D=20api=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biz/service/AnalyticsService.java | 38 ++---- .../biz/usecase/out/AIServicePort.java | 10 ++ .../infra/gateway/AIServiceAdapter.java | 112 ++++++++++++++++++ 3 files changed, 129 insertions(+), 31 deletions(-) diff --git a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java index 906c3ae..bc0aa7d 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java @@ -317,10 +317,8 @@ public class AnalyticsService implements AnalyticsUseCase { } /** - * LLM 기반 리뷰 감정 분석 - 한 번의 분석으로 긍정/부정/중립 수 모두 반환 - * - * @param reviews 분석할 리뷰 목록 - * @return ReviewSentimentCount 감정별 리뷰 수 + * 기존 analyzeReviewSentiments 메서드를 대량 분석 방식으로 개선 + * 개별 AI 호출 대신 한 번의 호출로 모든 리뷰 분석 */ private ReviewSentimentCount analyzeReviewSentiments(List reviews) { log.info("LLM 기반 리뷰 감정 분석 시작: 총 리뷰 수={}", reviews.size()); @@ -339,34 +337,12 @@ public class AnalyticsService implements AnalyticsUseCase { return new ReviewSentimentCount(0, 0, 0); } - int positiveCount = 0; - int negativeCount = 0; - int neutralCount = 0; + // 기존 개별 분석 대신 대량 분석 사용 + Map sentimentCounts = aiServicePort.analyzeBulkSentiments(validReviews); - // 각 리뷰를 AI로 감정 분석 - for (String review : validReviews) { - try { - SentimentType sentiment = aiServicePort.analyzeSentiment(review); - - switch (sentiment) { - case POSITIVE: - positiveCount++; - break; - case NEGATIVE: - negativeCount++; - break; - case NEUTRAL: - default: - neutralCount++; - break; - } - - } catch (Exception e) { - log.warn("개별 리뷰 감정 분석 실패, 중립으로 처리: {}", - review.substring(0, Math.min(30, review.length())), e); - neutralCount++; // 분석 실패 시 중립으로 처리 - } - } + int positiveCount = sentimentCounts.get(SentimentType.POSITIVE); + int negativeCount = sentimentCounts.get(SentimentType.NEGATIVE); + int neutralCount = sentimentCounts.get(SentimentType.NEUTRAL); ReviewSentimentCount result = new ReviewSentimentCount(positiveCount, negativeCount, neutralCount); diff --git a/analytics/src/main/java/com/ktds/hi/analytics/biz/usecase/out/AIServicePort.java b/analytics/src/main/java/com/ktds/hi/analytics/biz/usecase/out/AIServicePort.java index 30d4264..e003514 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/biz/usecase/out/AIServicePort.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/biz/usecase/out/AIServicePort.java @@ -4,6 +4,7 @@ import com.ktds.hi.analytics.biz.domain.AiFeedback; import com.ktds.hi.analytics.biz.domain.SentimentType; import java.util.List; +import java.util.Map; /** * AI 서비스 포트 인터페이스 @@ -20,6 +21,15 @@ public interface AIServicePort { * 감정 분석 */ SentimentType analyzeSentiment(String content); + + /** + * 대량 리뷰 감정 분석 (새로 추가) + * 여러 리뷰를 한 번에 분석하여 긍정/부정/중립 개수 반환 + * + * @param reviews 분석할 리뷰 목록 + * @return 감정 타입별 개수 맵 + */ + Map analyzeBulkSentiments(List reviews); /** * 실행 계획 생성 diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java index 5c167b0..0de96fa 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java @@ -30,8 +30,10 @@ import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * AI 서비스 어댑터 클래스 @@ -99,6 +101,116 @@ public class AIServiceAdapter implements AIServicePort { } } + @Override + public Map analyzeBulkSentiments(List reviews) { + log.info("대량 리뷰 감정 분석 시작: 리뷰 수={}", reviews.size()); + + try { + if (reviews.isEmpty()) { + return createEmptyResultMap(); + } + + // 유효한 리뷰만 필터링 + List validReviews = reviews.stream() + .filter(review -> review != null && !review.trim().isEmpty()) + .collect(Collectors.toList()); + + if (validReviews.isEmpty()) { + return createEmptyResultMap(); + } + + // 리뷰를 번호와 함께 포맷팅 + StringBuilder reviewsText = new StringBuilder(); + for (int i = 0; i < validReviews.size(); i++) { + reviewsText.append(String.format("%d. %s\n", i + 1, validReviews.get(i))); + } + + String prompt = String.format( + """ + 다음 리뷰들을 분석하여 긍정, 부정, 중립의 개수를 세어주세요. + + 리뷰 목록: + %s + + 결과를 다음 JSON 형식으로만 답변해주세요: + { + "positive": 긍정_개수, + "negative": 부정_개수, + "neutral": 중립_개수 + } + + 다른 설명은 하지 말고 JSON만 답변해주세요. + 긍정,부정,중립 개수를 모두 더했을때, 총 리뷰수와 동일해야 합니다. + 정확하게 세어주세요. + """, + reviewsText.toString() + ); + + // 기존 callOpenAI 메서드 활용 + String result = callOpenAI(prompt); + + // 결과 파싱 + Map sentimentMap = parseBulkSentimentResult(result, validReviews.size()); + + log.info("대량 리뷰 감정 분석 완료: 긍정={}, 부정={}, 중립={}", + sentimentMap.get(SentimentType.POSITIVE), + sentimentMap.get(SentimentType.NEGATIVE), + sentimentMap.get(SentimentType.NEUTRAL)); + + return sentimentMap; + + } catch (Exception e) { + log.error("대량 리뷰 감정 분석 중 오류 발생, fallback 사용", e); + return createFallbackResultMap(reviews.size()); + } + } + + private Map parseBulkSentimentResult(String result, int totalReviews) { + try { + // 기존 objectMapper 필드 사용 + Map jsonResult = objectMapper.readValue(result.trim(), Map.class); + + int positive = ((Number) jsonResult.getOrDefault("positive", 0)).intValue(); + int negative = ((Number) jsonResult.getOrDefault("negative", 0)).intValue(); + int neutral = ((Number) jsonResult.getOrDefault("neutral", 0)).intValue(); + + // 결과 검증 및 보정 + int totalAnalyzed = positive + negative + neutral; + if (totalAnalyzed != totalReviews) { + log.warn("분석 결과 불일치 보정: 분석된 수={}, 실제 리뷰 수={}", totalAnalyzed, totalReviews); + int difference = totalReviews - totalAnalyzed; + neutral += difference; + } + + Map resultMap = new HashMap<>(); + resultMap.put(SentimentType.POSITIVE, Math.max(0, positive)); + resultMap.put(SentimentType.NEGATIVE, Math.max(0, negative)); + resultMap.put(SentimentType.NEUTRAL, Math.max(0, neutral)); + + return resultMap; + + } catch (Exception e) { + log.error("대량 감정 분석 결과 파싱 실패: {}", result, e); + return createFallbackResultMap(totalReviews); + } + } + + private Map createEmptyResultMap() { + Map result = new HashMap<>(); + result.put(SentimentType.POSITIVE, 0); + result.put(SentimentType.NEGATIVE, 0); + result.put(SentimentType.NEUTRAL, 0); + return result; + } + + private Map createFallbackResultMap(int totalReviews) { + Map result = new HashMap<>(); + result.put(SentimentType.POSITIVE, (int) (totalReviews * 0.6)); + result.put(SentimentType.NEGATIVE, (int) (totalReviews * 0.2)); + result.put(SentimentType.NEUTRAL, totalReviews - (int) (totalReviews * 0.6) - (int) (totalReviews * 0.2)); + return result; + } + @Override public SentimentType analyzeSentiment(String content) { From cf9abb1b5403af36220be9c2948129b43d7c3649 Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Wed, 18 Jun 2025 15:49:41 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat=20:=20=EB=B6=84=EC=84=9D=20api=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(=EB=A6=AC=EB=B7=B0=20=EC=97=86=EC=9D=84?= =?UTF-8?q?=EC=8B=9C=20=EB=94=94=ED=8F=B4=ED=8A=B8=20=EA=B0=92=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ktds/hi/analytics/biz/service/AnalyticsService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java index bc0aa7d..c295630 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java @@ -276,8 +276,10 @@ public class AnalyticsService implements AnalyticsUseCase { .totalReviews(0) .positiveReviewCount(0) .negativeReviewCount(0) + .neutralReviewCount(0) .positiveRate(0.0) .negativeRate(0.0) + .neutralRate(0.0) .analysisDate(LocalDate.now()) .build(); From 2080c5476a444125f9b3fb4bb8e93a598dbd1d14 Mon Sep 17 00:00:00 2001 From: youbeen Date: Wed, 18 Jun 2025 16:02:30 +0900 Subject: [PATCH 4/5] store all --- dump.rdb | Bin 294 -> 493 bytes logs/recommend-service.log.2025-06-17.0.gz | Bin 0 -> 16602 bytes .../hi/store/biz/service/StoreService.java | 26 +++++++++++ .../hi/store/biz/usecase/in/StoreUseCase.java | 5 +++ .../infra/controller/StoreController.java | 14 ++++++ .../infra/dto/response/StoreListResponse.java | 42 ++++++++++++++++++ .../repository/StoreJpaRepository.java | 1 + 7 files changed, 88 insertions(+) create mode 100644 logs/recommend-service.log.2025-06-17.0.gz create mode 100644 store/src/main/java/com/ktds/hi/store/infra/dto/response/StoreListResponse.java diff --git a/dump.rdb b/dump.rdb index 1d341a29a40cc58a63ed21c79d805bd7915fa567..607a2d51f8fe0e07b16270401d6b28a1df8f6746 100644 GIT binary patch delta 316 zcmZ3+^p<&ofnZB)P{uEg(&E$<-Q3jNLlOrkI*Q6OG5uM2>EzPsj0_A60!685MXALZ z@g@1$sd-k01`ewxmMGTyI(rs-<^`E1yN71_XL?meg-2CJMfl}=W|n6rhPxSfX60A- zRfU%OW_g)OT@{ delta 146 zcmV;D0B!&61EvCyFcDP^QE2)Ib#rB8Ep26O!e|_kK@|r90sQjjjG&Vw0bgo(PDDX4 zPI_l^No`g%V?#+cO;lqsO+;fbPE<)VPe?E>acoUBLUwFWH#adiPHb&vG)h-cHfmW- zGBj9YUp6;Td1X;nLQ8ftMR!7MPE2n#MMyL_F-1ajLPvOMGht;-|7n{!jV{KI;&Mka AjQ{`u diff --git a/logs/recommend-service.log.2025-06-17.0.gz b/logs/recommend-service.log.2025-06-17.0.gz new file mode 100644 index 0000000000000000000000000000000000000000..cc1fa5653de409e881d22c59f220979f7a9d3c3b GIT binary patch literal 16602 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 z zA(#7uo$co<2&%jds%psTY4HPp5Eik7T5IgAwZ1TC?l?JEijcCQ%YjB+)KJB!IQV0@ z4nOrT)({q$qd(DKK;)fCPZ6da*7>=M4=H#|G7Q8B$-#-91?r*A8mx@_$e5UVZheL< z9LFHEspAIt(lYvfu_WSk%~$JMS)xauh-Yr)R!#qoYXrvMwj@_8U!vRk@A`?)r+y;Z zBzU%rWcTIQfMz8ev()>)^phTDlstfb5)o&d{PabyI77Sc2MZ8&R^Jqj#Ss-`afoGI z?}ZgG__oj8frddhioQv3g#vMa3>T?O9+zNJiJK$~ht>?#)akVT_e2DsPFpAvqB`>-pZoj-1UJl7)FkdW1vg@mHnKs~KAP})% zNw26-c{qaMgOFdxaHS>=OutDj9kj4%1pM!jVfOzQGBlfkOUcj{qYhL_j5;%CxG80N z?8g*x&Zs6O^uknm+KxrQSJe*;i3|@aBqV0fA0xzF+tx5U>XaeEn%7PRvSS_K;m;h^ zh5KTQ61KbCcQnhd^lL8-39LVxRwl5Q7BmbzZ2C4A45=iMt{}1Vn4(S~jbkiGvs>!v z#sb?psa)Okx3=K_3pD)hXD{VJ#vCkA(bTCoj=vbSkj8+Gz$MzSL>obfiaSE8s!r-! z7c8k>BYxRN+wj%5Z8aH^5y}>a@6$m)kTHmwXjL<)gD+w1{kp7l>&OV4-I@?6Ac?jHm1Jk?%OWVHSxQd@ z@`0dEssr;K_Pwr@6Glr~OC?DEd`c+iM~|)n%s3E8w%$O8M+YoUAkfpyr>pb$_o|r7 zKMxe)lWflmlQ(l9I4u~nq?8cXuWInir;Ba5T7ao(6w7QfsvrE&4`aUWh)^yCHR)qj z1cA@!nORgc>2Q*@umw4yA=JX8-A6c{yB}#Iat28fK62X&7x@{9zVw1WC=6{s;>2%1 z^m{9lnr_tHdYUcevDyRkfQc z8rBqyF7&w?H8#XP;adJ`q5MpcHZF94yP&rCY_zj2}&-W)?bT(~0j z?yTnWw|2?lPn=likP-d>BX}R4DJMmlIo#B1nMKrM{x$qAl@fkHUz%Z^#K zjhpGr_N*01H4&CIEEIY7fI3rI^`5sTO<0UE)JJKS$ccAM7$Yu?AH^P+GmUVCt}#-k zJdsUSVtfPDbmICEy9Re?;n$&VCi0BL)kuCRwf@&bKUqDoh}kd=qn~pU5pY(Su498t d6y+&_PYTL&4nlYhNQWPwU9z$vR&*dh{}&qEBZ>e3 literal 0 HcmV?d00001 diff --git a/store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java b/store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java index c8f0e7c..5e00998 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java +++ b/store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java @@ -3,6 +3,7 @@ package com.ktds.hi.store.biz.service; import com.ktds.hi.store.biz.usecase.in.StoreUseCase; import com.ktds.hi.store.infra.dto.*; +import com.ktds.hi.store.infra.dto.response.StoreListResponse; import com.ktds.hi.store.infra.gateway.entity.StoreEntity; import com.ktds.hi.store.infra.gateway.entity.TagEntity; import com.ktds.hi.store.infra.gateway.repository.StoreJpaRepository; @@ -93,6 +94,27 @@ public class StoreService implements StoreUseCase { .collect(Collectors.toList()); } + + @Override + public List getAllStores() { + + List stores = storeJpaRepository.findAll(); + + return stores.stream() + .map(store -> StoreListResponse.builder() + .storeId(store.getId()) + .storeName(store.getStoreName()) + .address(store.getAddress()) + .category(store.getCategory()) + .rating(store.getRating()) + .reviewCount(store.getReviewCount()) + .status("운영중") + .imageUrl(store.getImageUrl()) + .operatingHours(store.getOperatingHours()) + .build()) + .collect(Collectors.toList()); + } + @Override public StoreDetailResponse getStoreDetail(Long storeId) { @@ -157,6 +179,10 @@ public class StoreService implements StoreUseCase { .build(); } + + + + @Override public List searchStores(String keyword, String category, String tags, Double latitude, Double longitude, Integer radius, diff --git a/store/src/main/java/com/ktds/hi/store/biz/usecase/in/StoreUseCase.java b/store/src/main/java/com/ktds/hi/store/biz/usecase/in/StoreUseCase.java index c402215..3165a10 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/usecase/in/StoreUseCase.java +++ b/store/src/main/java/com/ktds/hi/store/biz/usecase/in/StoreUseCase.java @@ -1,6 +1,7 @@ package com.ktds.hi.store.biz.usecase.in; import com.ktds.hi.store.infra.dto.*; +import com.ktds.hi.store.infra.dto.response.StoreListResponse; import java.util.List; @@ -30,6 +31,8 @@ public interface StoreUseCase { */ List getMyStores(Long ownerId); + List getAllStores(); + /** * 매장 상세 조회 * @@ -73,4 +76,6 @@ public interface StoreUseCase { List searchStores(String keyword, String category, String tags, Double latitude, Double longitude, Integer radius, Integer page, Integer size); + + } diff --git a/store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java b/store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java index 35bffa7..78081c7 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java +++ b/store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java @@ -1,11 +1,14 @@ // store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java package com.ktds.hi.store.infra.controller; +import com.ktds.hi.store.biz.service.StoreService; import com.ktds.hi.store.biz.usecase.in.MenuUseCase; import com.ktds.hi.store.biz.usecase.in.StoreUseCase; +import com.ktds.hi.store.domain.Store; import com.ktds.hi.store.infra.dto.*; import com.ktds.hi.common.dto.ApiResponse; import com.ktds.hi.common.security.JwtTokenProvider; +import com.ktds.hi.store.infra.dto.response.StoreListResponse; import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -38,6 +41,7 @@ import java.util.List; public class StoreController { private final StoreUseCase storeUseCase; + private final StoreService storeService; private final JwtTokenProvider jwtTokenProvider; private final MenuUseCase menuUseCase; @@ -68,6 +72,14 @@ public class StoreController { return ResponseEntity.ok(ApiResponse.success(responses, "내 매장 목록 조회 완료")); } + @GetMapping("/stores/all") + @Operation(summary = "매장 전체 리스트") + public ResponseEntity>> getAllStores() { + + List responses = storeUseCase.getAllStores(); + return ResponseEntity.ok(ApiResponse.success(responses)); + } + @Operation(summary = "매장 상세 조회", description = "매장의 상세 정보를 조회합니다.") @GetMapping("/{storeId}") public ResponseEntity> getStoreDetail( @@ -133,6 +145,8 @@ public class StoreController { + + @GetMapping("/{storeId}/menus/popular") @Operation(summary = "매장 인기 메뉴 조회", description = "매장의 인기 메뉴(주문 횟수 기준)를 조회합니다.") public ResponseEntity>> getPopularMenus( diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/response/StoreListResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/response/StoreListResponse.java new file mode 100644 index 0000000..35cfa0f --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/response/StoreListResponse.java @@ -0,0 +1,42 @@ +package com.ktds.hi.store.infra.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "내 매장 목록 응답") +public class StoreListResponse { + + + @Schema(description = "매장 ID", example = "1") + private Long storeId; + + @Schema(description = "매장명", example = "맛집 한번 가볼래?") + private String storeName; + + @Schema(description = "주소", example = "서울시 강남구 테헤란로 123") + private String address; + + @Schema(description = "카테고리", example = "한식") + private String category; + + @Schema(description = "평점", example = "4.5") + private Double rating; + + @Schema(description = "리뷰 수", example = "127") + private Integer reviewCount; + + @Schema(description = "운영 상태", example = "운영중") + private String status; + + @Schema(description = "운영시간", example = "월-금 09:00-21:00") + private String operatingHours; + @Schema(description = "매장 이미지") + private String imageUrl; +} diff --git a/store/src/main/java/com/ktds/hi/store/infra/gateway/repository/StoreJpaRepository.java b/store/src/main/java/com/ktds/hi/store/infra/gateway/repository/StoreJpaRepository.java index 8f14021..387cfe9 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/gateway/repository/StoreJpaRepository.java +++ b/store/src/main/java/com/ktds/hi/store/infra/gateway/repository/StoreJpaRepository.java @@ -24,6 +24,7 @@ public interface StoreJpaRepository extends JpaRepository { @Query("SELECT s FROM StoreEntity s WHERE s.status = 'ACTIVE' ORDER BY s.rating DESC") Page findAllByOrderByRatingDesc(Pageable pageable); + /** * 점주 ID로 매장 목록 조회 */ From a24699bc8e895cb8cd1b5e8e442a25775d8cc263 Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Wed, 18 Jun 2025 16:03:15 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat=20:=20event=20hub=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java index e3eceb4..b856f12 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java @@ -43,7 +43,7 @@ public class EventHubAdapter { private final ExecutorService executorService = Executors.newFixedThreadPool(3); private volatile boolean isRunning = false; - @PostConstruct + // @PostConstruct public void startEventListening() { log.info("Event Hub 리스너 시작"); isRunning = true; @@ -52,7 +52,7 @@ public class EventHubAdapter { executorService.submit(this::listenToReviewEvents); } - @PreDestroy + // @PreDestroy public void stopEventListening() { log.info("Event Hub 리스너 종료"); isRunning = false;