From 714122726d576bef61ddea1cb6c8560a3a9d0530 Mon Sep 17 00:00:00 2001 From: UNGGU0704 Date: Tue, 17 Jun 2025 15:27:21 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Fix:=20redis=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20rediCache=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/ExternalPlatformAdapter.java | 174 ++++++++++++++---- 1 file changed, 134 insertions(+), 40 deletions(-) diff --git a/store/src/main/java/com/ktds/hi/store/infra/gateway/ExternalPlatformAdapter.java b/store/src/main/java/com/ktds/hi/store/infra/gateway/ExternalPlatformAdapter.java index 8530edb..c025780 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/gateway/ExternalPlatformAdapter.java +++ b/store/src/main/java/com/ktds/hi/store/infra/gateway/ExternalPlatformAdapter.java @@ -2,6 +2,7 @@ package com.ktds.hi.store.infra.gateway; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.ktds.hi.store.biz.usecase.out.CachePort; import com.ktds.hi.store.biz.usecase.out.ExternalPlatformPort; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,6 +16,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; /** @@ -29,6 +32,7 @@ public class ExternalPlatformAdapter implements ExternalPlatformPort { private final RestTemplate restTemplate; private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; + private final CachePort cachePort; @Value("${external.kakao.crawler.url:http://localhost:9001}") private String kakaoCrawlerUrl; @@ -145,11 +149,11 @@ public class ExternalPlatformAdapter implements ExternalPlatformPort { } /** - * 카카오 응답 파싱 및 Redis 저장 (단순화된 안정적인 방식) + * 카카오 응답 파싱 및 Redis 저장 (CacheAdapter 활용) */ private int parseAndStoreToRedis(Long storeId, String platform, String responseBody) { try { - log.info("카카오 API 응답: {}", responseBody); + log.info("카카오 API 응답 파싱 시작: storeId={}", storeId); if (responseBody == null || responseBody.trim().isEmpty()) { log.warn("카카오 응답이 비어있음: storeId={}", storeId); @@ -158,20 +162,35 @@ public class ExternalPlatformAdapter implements ExternalPlatformPort { JsonNode root = objectMapper.readTree(responseBody); - // 🔥 실제 카카오 응답 구조에 맞는 파싱 + // 실제 카카오 응답 구조 확인 if (!root.has("success") || !root.get("success").asBoolean()) { - log.warn("카카오 API 응답 실패: {}", responseBody); - updateSyncStatus(storeId, platform, "FAILED", 0); + log.warn("카카오 API 호출 실패: {}", root.path("message").asText()); + updateSyncStatusWithCache(storeId, platform, "FAILED", 0); return 0; } + // reviews 배열 직접 접근 JsonNode reviewsNode = root.get("reviews"); if (reviewsNode == null || !reviewsNode.isArray()) { log.warn("카카오 응답에 reviews 배열이 없음"); - updateSyncStatus(storeId, platform, "SUCCESS", 0); + updateSyncStatusWithCache(storeId, platform, "SUCCESS", 0); return 0; } + // 매장 정보 파싱 (옵션) + Map storeInfo = null; + if (root.has("store_info")) { + JsonNode storeInfoNode = root.get("store_info"); + storeInfo = new HashMap<>(); + storeInfo.put("id", storeInfoNode.path("id").asText()); + storeInfo.put("name", storeInfoNode.path("name").asText()); + storeInfo.put("category", storeInfoNode.path("category").asText()); + storeInfo.put("rating", storeInfoNode.path("rating").asText()); + storeInfo.put("reviewCount", storeInfoNode.path("review_count").asText()); + storeInfo.put("status", storeInfoNode.path("status").asText()); + storeInfo.put("address", storeInfoNode.path("address").asText()); + } + // 리뷰 데이터 파싱 List> reviews = new ArrayList<>(); for (JsonNode reviewNode : reviewsNode) { @@ -179,23 +198,35 @@ public class ExternalPlatformAdapter implements ExternalPlatformPort { // 기본 정보 review.put("reviewId", generateReviewId(reviewNode)); - review.put("content", reviewNode.path("content").asText("")); - review.put("rating", reviewNode.path("rating").asDouble(0.0)); review.put("reviewerName", reviewNode.path("reviewer_name").asText("")); - review.put("createdAt", reviewNode.path("date").asText("")); - review.put("platform", platform); - - // 카카오 특화 정보 review.put("reviewerLevel", reviewNode.path("reviewer_level").asText("")); + review.put("rating", reviewNode.path("rating").asInt(0)); + review.put("date", reviewNode.path("date").asText("")); + review.put("content", reviewNode.path("content").asText("")); review.put("likes", reviewNode.path("likes").asInt(0)); review.put("photoCount", reviewNode.path("photo_count").asInt(0)); review.put("hasPhotos", reviewNode.path("has_photos").asBoolean(false)); + review.put("platform", platform); - // 배지 정보 + // 리뷰어 통계 + if (reviewNode.has("reviewer_stats")) { + JsonNode statsNode = reviewNode.get("reviewer_stats"); + Map reviewerStats = new HashMap<>(); + reviewerStats.put("reviews", statsNode.path("reviews").asInt(0)); + reviewerStats.put("averageRating", statsNode.path("average_rating").asDouble(0.0)); + reviewerStats.put("followers", statsNode.path("followers").asInt(0)); + review.put("reviewerStats", reviewerStats); + } + + // 배지 if (reviewNode.has("badges") && reviewNode.get("badges").isArray()) { List badges = new ArrayList<>(); - reviewNode.get("badges").forEach(badge -> badges.add(badge.asText())); + for (JsonNode badgeNode : reviewNode.get("badges")) { + badges.add(badgeNode.asText()); + } review.put("badges", badges); + } else { + review.put("badges", new ArrayList()); } reviews.add(review); @@ -203,41 +234,20 @@ public class ExternalPlatformAdapter implements ExternalPlatformPort { log.info("파싱된 리뷰 수: {}", reviews.size()); - // Redis 저장 (단순한 방식) - saveToRedis(storeId, platform, reviews); + // CacheAdapter를 통한 Redis 저장 + saveToRedisWithCache(storeId, platform, reviews, storeInfo); // 동기화 상태 업데이트 - updateSyncStatus(storeId, platform, "SUCCESS", reviews.size()); + updateSyncStatusWithCache(storeId, platform, "SUCCESS", reviews.size()); return reviews.size(); } catch (Exception e) { - log.error("카카오 응답 파싱 및 Redis 저장 실패: storeId={}, error={}", storeId, e.getMessage(), e); - updateSyncStatus(storeId, platform, "FAILED", 0); + log.error("카카오 응답 파싱 실패: storeId={}, error={}", storeId, e.getMessage(), e); + updateSyncStatusWithCache(storeId, platform, "FAILED", 0); return 0; } } - - - - /** - * 🔥 누락된 generateReviewId 메서드 추가 - */ - private String generateReviewId(JsonNode reviewNode) { - try { - String reviewerName = reviewNode.path("reviewer_name").asText(""); - String date = reviewNode.path("date").asText(""); - String content = reviewNode.path("content").asText(""); - - // 고유한 ID 생성 (리뷰어명 + 날짜 + 컨텐츠 해시) - String combined = reviewerName + "_" + date + "_" + content.hashCode(); - return "kakao_" + Math.abs(combined.hashCode()); - } catch (Exception e) { - // 예외 발생 시 타임스탬프 기반 ID 생성 - return "kakao_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000); - } - } - // ===== 계정 연동 메서드들 ===== @Override @@ -580,6 +590,90 @@ public class ExternalPlatformAdapter implements ExternalPlatformPort { } catch (Exception e) { log.error("Redis 저장 실패: storeId={}, platform={}, error={}", storeId, platform, e.getMessage()); + e.printStackTrace(); + } + } + + + /** + * CacheAdapter를 통한 Redis 저장 (안전한 방식) + */ + private void saveToRedisWithCache(Long storeId, String platform, List> reviews, Map storeInfo) { + try { + long timestamp = System.currentTimeMillis(); + String redisKey = String.format("external:reviews:pending:%d:%s:%d", storeId, platform, timestamp); + + Map cacheData = new HashMap<>(); + cacheData.put("storeId", storeId); + cacheData.put("platform", platform); + cacheData.put("reviews", reviews); + cacheData.put("reviewCount", reviews.size()); + cacheData.put("timestamp", timestamp); + cacheData.put("status", "PENDING"); + cacheData.put("syncTime", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + + if (storeInfo != null) { + cacheData.put("storeInfo", storeInfo); + } + + // 기존 CacheAdapter 활용 (TTL 1일 = 86400초) + cachePort.putStoreCache(redisKey, cacheData, 86400); + + log.info("CacheAdapter로 리뷰 데이터 저장 완료: key={}, reviewCount={}, hasStoreInfo={}", + redisKey, reviews.size(), storeInfo != null); + + } catch (Exception e) { + log.error("CacheAdapter 저장 실패: storeId={}, platform={}, error={}", storeId, platform, e.getMessage()); + } + } + + /** + * CacheAdapter를 통한 동기화 상태 업데이트 + */ + private void updateSyncStatusWithCache(Long storeId, String platform, String status, int count) { + try { + String statusKey = String.format("external:sync:status:%d:%s", storeId, platform); + + Map statusData = new HashMap<>(); + statusData.put("storeId", storeId); + statusData.put("platform", platform); + statusData.put("status", status); + statusData.put("syncedCount", count); + statusData.put("timestamp", System.currentTimeMillis()); + + // CacheAdapter 활용 (TTL 1일) + cachePort.putStoreCache(statusKey, statusData, 86400); + + log.info("CacheAdapter로 동기화 상태 저장 완료: storeId={}, platform={}, status={}, count={}", + storeId, platform, status, count); + + } catch (Exception e) { + log.warn("CacheAdapter 상태 저장 실패: storeId={}, platform={}, error={}", + storeId, platform, e.getMessage()); + } + } + + /** + * 리뷰 ID 생성 + */ + private String generateReviewId(JsonNode reviewNode) { + try { + String reviewerName = reviewNode.path("reviewer_name").asText(""); + String date = reviewNode.path("date").asText(""); + String content = reviewNode.path("content").asText(""); + + String combined = reviewerName + "|" + date + "|" + content.substring(0, Math.min(50, content.length())); + int hash = Math.abs(combined.hashCode()); + + return String.format("kakao_%s_%d_%d", + date.replaceAll("[^0-9]", ""), + hash, + System.currentTimeMillis() % 1000); + + } catch (Exception e) { + return String.format("kakao_fallback_%d_%d", + System.currentTimeMillis(), + (int)(Math.random() * 10000)); } } From d1128a27a289d314455a0913a43f0f6e36fea796 Mon Sep 17 00:00:00 2001 From: UNGGU0704 Date: Tue, 17 Jun 2025 15:39:59 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Chore:=20redis=20logging=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/ktds/hi/store/infra/gateway/CacheAdapter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/store/src/main/java/com/ktds/hi/store/infra/gateway/CacheAdapter.java b/store/src/main/java/com/ktds/hi/store/infra/gateway/CacheAdapter.java index b2f80cc..b63ffb0 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/gateway/CacheAdapter.java +++ b/store/src/main/java/com/ktds/hi/store/infra/gateway/CacheAdapter.java @@ -42,6 +42,7 @@ public class CacheAdapter implements CachePort { log.debug("캐시 저장: key={}, ttl={}초", key, ttlSeconds); } catch (Exception e) { log.warn("캐시 저장 실패: key={}, error={}", key, e.getMessage()); + e.printStackTrace(); } } From aa0d531399d1d017b3a99add70c115ff13a60885 Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Tue, 17 Jun 2025 15:40:24 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat=20:=20common=20=EC=98=A4=EB=A5=98=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 --- common/src/main/resources/application-common.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/resources/application-common.yml b/common/src/main/resources/application-common.yml index 0ce9faa..f7ecdf3 100644 --- a/common/src/main/resources/application-common.yml +++ b/common/src/main/resources/application-common.yml @@ -20,7 +20,7 @@ spring: # Redis 설정 data: redis: - host: ${REDIS_HOST:localhost} //로컬 + host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD:} timeout: 2000ms From 93914e13f9f57eacdaf9e86ab232cc6f667c1434 Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Tue, 17 Jun 2025 16:41:31 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat=20:=20=EB=AA=A8=EB=93=A0=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=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 --- .../infra/controller/AnalyticsController.java | 3 ++ .../ktds/hi/store/biz/service/TagService.java | 19 ++++++++++++ .../hi/store/biz/usecase/in/TagUseCase.java | 8 +++++ .../store/infra/controller/TagController.java | 13 ++++++++ .../infra/dto/response/AllTagResponse.java | 31 +++++++++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 store/src/main/java/com/ktds/hi/store/infra/dto/response/AllTagResponse.java diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/controller/AnalyticsController.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/controller/AnalyticsController.java index 16f89d2..119510b 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/controller/AnalyticsController.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/controller/AnalyticsController.java @@ -13,6 +13,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import jakarta.validation.constraints.*; @@ -151,8 +152,10 @@ public class AnalyticsController { @Parameter(description = "AI 피드백 ID", required = true) @PathVariable @NotNull Long feedbackId, @RequestBody ActionPlanCreateRequest request, + @AuthenticationPrincipal long id, HttpServletRequest httpRequest) { + System.out.println("test => " + id); // validation 체크 if (request.getActionPlanSelect() == null || request.getActionPlanSelect().isEmpty()) { diff --git a/store/src/main/java/com/ktds/hi/store/biz/service/TagService.java b/store/src/main/java/com/ktds/hi/store/biz/service/TagService.java index 9fcf1a9..12c681b 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/service/TagService.java +++ b/store/src/main/java/com/ktds/hi/store/biz/service/TagService.java @@ -3,6 +3,7 @@ package com.ktds.hi.store.biz.service; import com.ktds.hi.store.biz.usecase.in.TagUseCase; import com.ktds.hi.store.biz.usecase.out.TagRepositoryPort; import com.ktds.hi.store.domain.Tag; +import com.ktds.hi.store.infra.dto.response.AllTagResponse; import com.ktds.hi.store.infra.dto.response.TopClickedTagResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,6 +29,24 @@ public class TagService implements TagUseCase { private final TagRepositoryPort tagRepositoryPort; + @Override + public List getAllTags() { + log.info("모든 활성화된 태그 목록 조회 시작"); + + List tags = tagRepositoryPort.findAllActiveTags(); + + List responses = tags.stream() + .map(tag -> AllTagResponse.builder() + .id(tag.getId()) + .tagCategory(tag.getTagCategory().name()) + .tagName(tag.getTagName()) + .build()) + .collect(Collectors.toList()); + + log.info("모든 활성화된 태그 목록 조회 완료: count={}", responses.size()); + return responses; + } + @Override public List getTopClickedTags() { log.info("가장 많이 클릭된 상위 5개 태그 조회 시작"); diff --git a/store/src/main/java/com/ktds/hi/store/biz/usecase/in/TagUseCase.java b/store/src/main/java/com/ktds/hi/store/biz/usecase/in/TagUseCase.java index ac15128..3a178aa 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/usecase/in/TagUseCase.java +++ b/store/src/main/java/com/ktds/hi/store/biz/usecase/in/TagUseCase.java @@ -1,5 +1,6 @@ package com.ktds.hi.store.biz.usecase.in; +import com.ktds.hi.store.infra.dto.response.AllTagResponse; import com.ktds.hi.store.infra.dto.response.TopClickedTagResponse; import java.util.List; @@ -13,6 +14,13 @@ import java.util.List; */ public interface TagUseCase { + /** + * 모든 활성화된 태그 목록 조회 + * + * @return 모든 태그 목록 + */ + List getAllTags(); + /** * 가장 많이 클릭된 상위 5개 태그 조회 */ diff --git a/store/src/main/java/com/ktds/hi/store/infra/controller/TagController.java b/store/src/main/java/com/ktds/hi/store/infra/controller/TagController.java index ad467ba..6f83672 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/controller/TagController.java +++ b/store/src/main/java/com/ktds/hi/store/infra/controller/TagController.java @@ -1,6 +1,7 @@ package com.ktds.hi.store.infra.controller; import com.ktds.hi.store.biz.usecase.in.TagUseCase; +import com.ktds.hi.store.infra.dto.response.AllTagResponse; import com.ktds.hi.store.infra.dto.response.TopClickedTagResponse; import com.ktds.hi.common.dto.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -26,6 +27,18 @@ public class TagController { private final TagUseCase tagUseCase; + /** + * 모든 활성화된 태그 목록 조회 API + */ + @GetMapping + @Operation(summary = "모든 태그 조회", description = "활성화된 모든 태그 목록을 조회합니다.") + public ResponseEntity>> getAllTags() { + + List tags = tagUseCase.getAllTags(); + + return ResponseEntity.ok(ApiResponse.success(tags)); + } + /** * 가장 많이 클릭된 상위 5개 태그 조회 API */ diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/response/AllTagResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/response/AllTagResponse.java new file mode 100644 index 0000000..50fbbb4 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/response/AllTagResponse.java @@ -0,0 +1,31 @@ +package com.ktds.hi.store.infra.dto.response; + +import lombok.Builder; +import lombok.Getter; + +/** + * 모든 태그 응답 DTO + * 태그 기본 정보를 담는 응답 클래스 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Getter +@Builder +public class AllTagResponse { + + /** + * 태그 ID + */ + private Long id; + + /** + * 태그 카테고리 + */ + private String tagCategory; + + /** + * 태그명 + */ + private String tagName; +}