mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2026-06-12 20:39:09 +00:00
fix: git merge error fix
This commit is contained in:
+11
-177
@@ -103,14 +103,10 @@ public class MarketingTipService implements MarketingTipUseCase {
|
||||
String aiGeneratedTip = aiTipGenerator.generateTip(storeWithMenuData);
|
||||
log.debug("AI 팁 생성 완료: {}", aiGeneratedTip.substring(0, Math.min(50, aiGeneratedTip.length())));
|
||||
|
||||
String tipSummary = generateTipSummary(aiGeneratedTip);
|
||||
log.info("tipSummary : {}", tipSummary);
|
||||
|
||||
// 도메인 객체 생성 및 저장
|
||||
MarketingTip marketingTip = MarketingTip.builder()
|
||||
.storeId(storeWithMenuData.getStoreData().getStoreId())
|
||||
.tipContent(aiGeneratedTip)
|
||||
.tipSummary(tipSummary)
|
||||
.storeWithMenuData(storeWithMenuData)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
@@ -126,10 +122,11 @@ public class MarketingTipService implements MarketingTipUseCase {
|
||||
* 마케팅 팁을 응답 DTO로 변환 (전체 내용 포함)
|
||||
*/
|
||||
private MarketingTipResponse convertToResponse(MarketingTip marketingTip, StoreData storeData, boolean isRecentlyCreated) {
|
||||
String tipSummary = generateTipSummary(marketingTip.getTipContent());
|
||||
|
||||
return MarketingTipResponse.builder()
|
||||
.tipId(marketingTip.getId().getValue())
|
||||
.tipSummary(marketingTip.getTipSummary())
|
||||
.tipSummary(tipSummary)
|
||||
.tipContent(marketingTip.getTipContent()) // 🆕 전체 내용 포함
|
||||
.storeInfo(MarketingTipResponse.StoreInfo.builder()
|
||||
.storeName(storeData.getStoreName())
|
||||
@@ -142,187 +139,24 @@ public class MarketingTipService implements MarketingTipUseCase {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 마케팅 팁 요약 생성 (첫 50자 또는 첫 번째 문장)
|
||||
*/
|
||||
private String generateTipSummary(String fullContent) {
|
||||
if (fullContent == null || fullContent.trim().isEmpty()) {
|
||||
return "마케팅 팁이 생성되었습니다.";
|
||||
}
|
||||
|
||||
try {
|
||||
// JSON 형식 처리: "```html\n..." 패턴
|
||||
String processedContent = preprocessContent(fullContent);
|
||||
|
||||
// 1순위: HTML 블록 밖의 첫 번째 제목 추출
|
||||
String titleOutsideHtml = extractTitleOutsideHtml(processedContent);
|
||||
if (titleOutsideHtml != null && titleOutsideHtml.length() > 5) {
|
||||
return titleOutsideHtml;
|
||||
}
|
||||
|
||||
// 2순위: <b> 태그 안의 첫 번째 내용 추출
|
||||
String boldContent = extractBoldContent(processedContent);
|
||||
if (boldContent != null && boldContent.length() > 5) {
|
||||
return boldContent;
|
||||
}
|
||||
|
||||
// 3순위: HTML 태그 제거 후 첫 번째 문장
|
||||
return extractFirstSentence(processedContent);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("마케팅 팁 요약 생성 중 오류", e);
|
||||
return "마케팅 팁이 생성되었습니다.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON이나 특수 형식 전처리
|
||||
*/
|
||||
private String preprocessContent(String content) {
|
||||
// 먼저 JSON 이스케이프 문자 정리
|
||||
if (content.contains("\\n")) {
|
||||
content = content.replaceAll("\\\\n", "\n");
|
||||
}
|
||||
|
||||
// JSON 구조에서 실제 HTML 내용만 추출
|
||||
if (content.contains("```html")) {
|
||||
content = content.replaceAll("```html", "")
|
||||
.replaceAll("```", "")
|
||||
.replaceAll("\"", "");
|
||||
}
|
||||
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 블록 밖의 첫 번째 제목 라인 추출
|
||||
* ```html 이후 첫 번째 줄의 내용만 추출
|
||||
*/
|
||||
private String extractTitleOutsideHtml(String content) {
|
||||
// 먼저 이스케이프 문자 정리
|
||||
String processedContent = content.replaceAll("\\\\n", "\n");
|
||||
|
||||
// ```html 패턴 찾기 (이스케이프 처리 후)
|
||||
String[] htmlPatterns = {"```html\n", "```html\\n"};
|
||||
|
||||
for (String pattern : htmlPatterns) {
|
||||
int htmlStart = processedContent.indexOf(pattern);
|
||||
if (htmlStart != -1) {
|
||||
// 패턴 이후부터 시작
|
||||
int contentStart = htmlStart + pattern.length();
|
||||
|
||||
// 첫 번째 줄바꿈까지 또는 \n\n까지 찾기
|
||||
String remaining = processedContent.substring(contentStart);
|
||||
String[] lines = remaining.split("\n");
|
||||
|
||||
if (lines.length > 0) {
|
||||
String firstLine = lines[0].trim();
|
||||
|
||||
// 유효한 내용인지 확인
|
||||
if (firstLine.length() > 5 && !firstLine.contains("🎯") && !firstLine.contains("<")) {
|
||||
return cleanText(firstLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 방식으로 fallback
|
||||
return extractFromLines(processedContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 줄별로 처리하는 기존 방식
|
||||
*/
|
||||
private String extractFromLines(String content) {
|
||||
String[] lines = content.split("\n");
|
||||
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
|
||||
// 빈 줄이나 HTML 태그, 이모지로 시작하는 줄 건너뛰기
|
||||
if (line.isEmpty() ||
|
||||
line.contains("<") ||
|
||||
line.startsWith("🎯") ||
|
||||
line.startsWith("🔍") ||
|
||||
line.equals("```html") ||
|
||||
line.matches("^[\\p{So}\\p{Sk}\\s]+$")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 의미있는 제목 라인 발견
|
||||
if (line.length() > 5) {
|
||||
return cleanText(line);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b> 태그 안의 첫 번째 내용 추출
|
||||
*/
|
||||
private String extractBoldContent(String htmlContent) {
|
||||
int startIndex = htmlContent.indexOf("<b>");
|
||||
if (startIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int endIndex = htmlContent.indexOf("</b>", startIndex);
|
||||
if (endIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String content = htmlContent.substring(startIndex + 3, endIndex).trim();
|
||||
return cleanText(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 정리
|
||||
*/
|
||||
private String cleanText(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return text.replaceAll(" ", " ")
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 태그 제거 후 첫 번째 의미있는 문장 추출
|
||||
*/
|
||||
private String extractFirstSentence(String htmlContent) {
|
||||
// HTML 태그 모두 제거
|
||||
String cleanContent = htmlContent.replaceAll("<[^>]+>", "").trim();
|
||||
|
||||
// 줄별로 나누어서 첫 번째 의미있는 줄 찾기
|
||||
String[] lines = cleanContent.split("\\n");
|
||||
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
|
||||
// 빈 줄이나 이모지만 있는 줄 건너뛰기
|
||||
if (line.isEmpty() || line.matches("^[\\p{So}\\p{Sk}\\s]+$")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 최소 길이 체크하고 반환
|
||||
if (line.length() > 5) {
|
||||
// 50자 제한
|
||||
if (line.length() > 50) {
|
||||
return line.substring(0, 50).trim() + "...";
|
||||
}
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 방법이 실패하면 기존 방식 사용
|
||||
String[] sentences = cleanContent.split("[.!?]");
|
||||
String firstSentence = sentences.length > 0 ? sentences[0].trim() : cleanContent;
|
||||
// 첫 번째 문장으로 요약 (마침표 기준)
|
||||
String[] sentences = fullContent.split("[.!?]");
|
||||
String firstSentence = sentences.length > 0 ? sentences[0].trim() : fullContent;
|
||||
|
||||
// 50자 제한
|
||||
if (firstSentence.length() > 50) {
|
||||
firstSentence = firstSentence.substring(0, 50).trim() + "...";
|
||||
return firstSentence.substring(0, 47) + "...";
|
||||
}
|
||||
|
||||
return firstSentence.isEmpty() ? "마케팅 팁이 생성되었습니다." : firstSentence;
|
||||
return firstSentence;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package com.won.smarketing.recommend.infrastructure.external;
|
||||
|
||||
import com.won.smarketing.recommend.domain.model.MenuData;
|
||||
import com.won.smarketing.recommend.domain.model.StoreData;
|
||||
import com.won.smarketing.recommend.domain.model.StoreWithMenuData;
|
||||
import com.won.smarketing.recommend.domain.service.AiTipGenerator;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service; // 이 어노테이션이 누락되어 있었음
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Python AI 팁 생성 구현체 (날씨 정보 제거)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service // 추가된 어노테이션
|
||||
@RequiredArgsConstructor
|
||||
public class PythonAiTipGenerator implements AiTipGenerator {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
@Value("${external.python-ai-service.base-url}")
|
||||
private String pythonAiServiceBaseUrl;
|
||||
|
||||
@Value("${external.python-ai-service.api-key}")
|
||||
private String pythonAiServiceApiKey;
|
||||
|
||||
@Value("${external.python-ai-service.timeout}")
|
||||
private int timeout;
|
||||
|
||||
@Override
|
||||
public String generateTip(StoreWithMenuData storeWithMenuData) {
|
||||
try {
|
||||
log.debug("Python AI 서비스 직접 호출: store={}", storeWithMenuData.getStoreData().getStoreName());
|
||||
return callPythonAiService(storeWithMenuData);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Python AI 서비스 호출 실패, Fallback 처리: {}", e.getMessage());
|
||||
return createFallbackTip(storeWithMenuData);
|
||||
}
|
||||
}
|
||||
|
||||
private String callPythonAiService(StoreWithMenuData storeWithMenuData) {
|
||||
|
||||
try {
|
||||
|
||||
StoreData storeData = storeWithMenuData.getStoreData();
|
||||
List<MenuData> menuDataList = storeWithMenuData.getMenuDataList();
|
||||
|
||||
// 메뉴 데이터를 Map 형태로 변환
|
||||
List<Map<String, Object>> menuList = menuDataList.stream()
|
||||
.map(menu -> {
|
||||
Map<String, Object> menuMap = new HashMap<>();
|
||||
menuMap.put("menu_id", menu.getMenuId());
|
||||
menuMap.put("menu_name", menu.getMenuName());
|
||||
menuMap.put("category", menu.getCategory());
|
||||
menuMap.put("price", menu.getPrice());
|
||||
menuMap.put("description", menu.getDescription());
|
||||
return menuMap;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Python AI 서비스로 전송할 데이터 (매장 정보 + 메뉴 정보)
|
||||
Map<String, Object> requestData = new HashMap<>();
|
||||
requestData.put("store_name", storeData.getStoreName());
|
||||
requestData.put("business_type", storeData.getBusinessType());
|
||||
requestData.put("location", storeData.getLocation());
|
||||
requestData.put("seat_count", storeData.getSeatCount());
|
||||
requestData.put("menu_list", menuList);
|
||||
|
||||
log.debug("Python AI 서비스 요청 데이터: {}", requestData);
|
||||
|
||||
PythonAiResponse response = webClient
|
||||
.post()
|
||||
.uri(pythonAiServiceBaseUrl + "/api/v1/generate-marketing-tip")
|
||||
.header("Authorization", "Bearer " + pythonAiServiceApiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.bodyValue(requestData)
|
||||
.retrieve()
|
||||
.bodyToMono(PythonAiResponse.class)
|
||||
.timeout(Duration.ofMillis(timeout))
|
||||
.block();
|
||||
|
||||
if (response != null && response.getTip() != null && !response.getTip().trim().isEmpty()) {
|
||||
log.debug("Python AI 서비스 응답 성공: tip length={}", response.getTip().length());
|
||||
return response.getTip();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Python AI 서비스 실제 호출 실패: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return createFallbackTip(storeWithMenuData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 규칙 기반 Fallback 팁 생성 (날씨 정보 없이 매장 정보만 활용)
|
||||
*/
|
||||
private String createFallbackTip(StoreWithMenuData storeWithMenuData) {
|
||||
String businessType = storeWithMenuData.getStoreData().getBusinessType();
|
||||
String storeName = storeWithMenuData.getStoreData().getStoreName();
|
||||
String location = storeWithMenuData.getStoreData().getLocation();
|
||||
|
||||
// 업종별 기본 팁 생성
|
||||
if (businessType.contains("카페")) {
|
||||
return String.format("%s만의 시그니처 음료와 디저트로 고객들에게 특별한 경험을 선사해보세요!", storeName);
|
||||
} else if (businessType.contains("음식점") || businessType.contains("식당")) {
|
||||
return String.format("%s의 대표 메뉴를 활용한 특별한 이벤트로 고객들의 관심을 끌어보세요!", storeName);
|
||||
} else if (businessType.contains("베이커리") || businessType.contains("빵집")) {
|
||||
return String.format("%s의 갓 구운 빵과 함께하는 따뜻한 서비스로 고객들의 마음을 사로잡아보세요!", storeName);
|
||||
} else if (businessType.contains("치킨") || businessType.contains("튀김")) {
|
||||
return String.format("%s의 바삭하고 맛있는 메뉴로 고객들에게 만족스러운 식사를 제공해보세요!", storeName);
|
||||
}
|
||||
|
||||
// 지역별 팁
|
||||
if (location.contains("강남") || location.contains("서초")) {
|
||||
return String.format("%s에서 트렌디하고 세련된 서비스로 젊은 고객층을 공략해보세요!", storeName);
|
||||
} else if (location.contains("홍대") || location.contains("신촌")) {
|
||||
return String.format("%s에서 활기차고 개성 있는 이벤트로 대학생들의 관심을 끌어보세요!", storeName);
|
||||
}
|
||||
|
||||
// 기본 팁
|
||||
return String.format("%s만의 특별함을 살린 고객 맞춤 서비스로 단골 고객을 늘려보세요!", storeName);
|
||||
}
|
||||
|
||||
@Getter
|
||||
private static class PythonAiResponse {
|
||||
private String tip;
|
||||
private String status;
|
||||
private String message;
|
||||
private LocalDateTime generatedTip;
|
||||
private String businessType;
|
||||
private String aiModel;
|
||||
}
|
||||
}
|
||||
@@ -33,12 +33,15 @@ external:
|
||||
api-key: ${PYTHON_AI_API_KEY:dummy-key}
|
||||
timeout: ${PYTHON_AI_TIMEOUT:30000}
|
||||
|
||||
<<<<<<< HEAD
|
||||
azure:
|
||||
eventhub:
|
||||
namespace: ${AZURE_EVENTHUB_NAMESPACE}
|
||||
marketing-tip-hub: ${AZURE_EVENTHUB_MARKETING_TIP_HUB:marketing-tip-requests}
|
||||
consumer-group: ${AZURE_EVENTHUB_CONSUMER_GROUP:ai-recommend-service}
|
||||
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
|
||||
Reference in New Issue
Block a user