mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2025-12-06 07:06:24 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
642e643167
5
.idea/.gitignore
generated
vendored
5
.idea/.gitignore
generated
vendored
@ -1,5 +0,0 @@
|
|||||||
# 디폴트 무시된 파일
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 환경에 따라 달라지는 Maven 홈 디렉터리
|
|
||||||
/mavenHomeManager.xml
|
|
||||||
11
.idea/gradle.xml
generated
11
.idea/gradle.xml
generated
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GradleSettings">
|
|
||||||
<option name="linkedExternalProjectsSettings">
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
</GradleProjectSettings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
</project>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
109
.idea/workspace.xml
generated
Normal file
109
.idea/workspace.xml
generated
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="7d9c48b3-e5c8-4a1c-af9a-469e24fa5715" name="변경" comment="">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/gradle.xml" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="ExternalProjectsData">
|
||||||
|
<projectState path="$PROJECT_DIR$">
|
||||||
|
<ProjectState />
|
||||||
|
</projectState>
|
||||||
|
</component>
|
||||||
|
<component name="ExternalProjectsManager">
|
||||||
|
<system id="GRADLE">
|
||||||
|
<state>
|
||||||
|
<task path="$PROJECT_DIR$">
|
||||||
|
<activation />
|
||||||
|
</task>
|
||||||
|
<projects_view />
|
||||||
|
</state>
|
||||||
|
</system>
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"customColor": "",
|
||||||
|
"associatedIndex": 4
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="2yLeuaqHXgKgtNCa4XzAZzifagS" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"Gradle.member.executor": "Run",
|
||||||
|
"Gradle.소스 다운로드.executor": "Run",
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"git-widget-placeholder": "main",
|
||||||
|
"last_opened_file_path": "C:/home/workspace/smarketing/smarketing-backend",
|
||||||
|
"project.structure.last.edited": "SDK",
|
||||||
|
"project.structure.proportion": "0.15",
|
||||||
|
"project.structure.side.proportion": "0.2",
|
||||||
|
"settings.editor.selected.configurable": "reference.settingsdialog.project.gradle"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="RunDashboard">
|
||||||
|
<option name="configurationTypes">
|
||||||
|
<set>
|
||||||
|
<option value="GradleRunConfiguration" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="RunManager">
|
||||||
|
<configuration name="member" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="env">
|
||||||
|
<map>
|
||||||
|
<entry key="POSTGRES_HOST" value="psql-digitalgarage-02.postgres.database.azure.com" />
|
||||||
|
<entry key="POSTGRES_PASSWORD" value="DG_Won!" />
|
||||||
|
<entry key="POSTGRES_USER" value="pgadmin" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="passParentEnvs" value="false" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":member:bootRun" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="디폴트 작업">
|
||||||
|
<changelist id="7d9c48b3-e5c8-4a1c-af9a-469e24fa5715" name="변경" comment="" />
|
||||||
|
<created>1749618504890</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1749618504890</updated>
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -2,18 +2,16 @@ package com.won.smarketing.recommend;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 추천 서비스 메인 애플리케이션 클래스
|
* AI 추천 서비스 메인 애플리케이션
|
||||||
* Clean Architecture 패턴을 적용한 AI 마케팅 추천 서비스
|
|
||||||
*/
|
*/
|
||||||
@SpringBootApplication(scanBasePackages = {"com.won.smarketing.recommend", "com.won.smarketing.common"})
|
@SpringBootApplication
|
||||||
@EntityScan(basePackages = {"com.won.smarketing.recommend.infrastructure.entity"})
|
@EnableJpaAuditing
|
||||||
@EnableJpaRepositories(basePackages = {"com.won.smarketing.recommend.infrastructure.repository"})
|
@EnableCaching
|
||||||
public class AIRecommendServiceApplication {
|
public class AIRecommendServiceApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(AIRecommendServiceApplication.class, args);
|
SpringApplication.run(AIRecommendServiceApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.won.smarketing.recommend.domain.service;
|
||||||
|
|
||||||
|
import com.won.smarketing.recommend.domain.model.StoreData;
|
||||||
|
import com.won.smarketing.recommend.domain.model.WeatherData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python AI 서비스 인터페이스
|
||||||
|
* AI 처리를 Python 서비스로 위임하는 도메인 서비스
|
||||||
|
*/
|
||||||
|
public interface AiApiService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python AI 서비스를 통한 마케팅 팁 생성
|
||||||
|
*
|
||||||
|
* @param storeData 매장 정보
|
||||||
|
* @param weatherData 날씨 정보
|
||||||
|
* @param additionalRequirement 추가 요청사항
|
||||||
|
* @return AI가 생성한 마케팅 팁 (한 줄)
|
||||||
|
*/
|
||||||
|
String generateMarketingTip(StoreData storeData, WeatherData weatherData, String additionalRequirement);
|
||||||
|
}
|
||||||
@ -8,26 +8,26 @@ import com.won.smarketing.recommend.domain.model.StoreData;
|
|||||||
import com.won.smarketing.recommend.domain.model.TipId;
|
import com.won.smarketing.recommend.domain.model.TipId;
|
||||||
import com.won.smarketing.recommend.domain.model.WeatherData;
|
import com.won.smarketing.recommend.domain.model.WeatherData;
|
||||||
import com.won.smarketing.recommend.domain.repository.MarketingTipRepository;
|
import com.won.smarketing.recommend.domain.repository.MarketingTipRepository;
|
||||||
import com.won.smarketing.recommend.domain.service.AiTipGenerator;
|
|
||||||
import com.won.smarketing.recommend.domain.service.StoreDataProvider;
|
import com.won.smarketing.recommend.domain.service.StoreDataProvider;
|
||||||
import com.won.smarketing.recommend.domain.service.WeatherDataProvider;
|
import com.won.smarketing.recommend.domain.service.WeatherDataProvider;
|
||||||
|
import com.won.smarketing.recommend.domain.service.AiTipGenerator;
|
||||||
import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest;
|
import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest;
|
||||||
import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse;
|
import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 마케팅 팁 서비스 구현체
|
* 마케팅 팁 서비스 구현체
|
||||||
* AI 기반 마케팅 팁 생성 및 저장 기능 구현
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Transactional(readOnly = true)
|
@Transactional
|
||||||
public class MarketingTipService implements MarketingTipUseCase {
|
public class MarketingTipService implements MarketingTipUseCase {
|
||||||
|
|
||||||
private final MarketingTipRepository marketingTipRepository;
|
private final MarketingTipRepository marketingTipRepository;
|
||||||
@ -35,49 +35,95 @@ public class MarketingTipService implements MarketingTipUseCase {
|
|||||||
private final WeatherDataProvider weatherDataProvider;
|
private final WeatherDataProvider weatherDataProvider;
|
||||||
private final AiTipGenerator aiTipGenerator;
|
private final AiTipGenerator aiTipGenerator;
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 마케팅 팁 생성
|
|
||||||
*
|
|
||||||
* @param request 마케팅 팁 생성 요청
|
|
||||||
* @return 생성된 마케팅 팁 응답
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
|
||||||
public MarketingTipResponse generateMarketingTips(MarketingTipRequest request) {
|
public MarketingTipResponse generateMarketingTips(MarketingTipRequest request) {
|
||||||
|
log.info("마케팅 팁 생성 시작: storeId={}", request.getStoreId());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 매장 정보 조회
|
// 1. 매장 정보 조회
|
||||||
StoreData storeData = storeDataProvider.getStoreData(request.getStoreId());
|
StoreData storeData = storeDataProvider.getStoreData(request.getStoreId());
|
||||||
log.debug("매장 정보 조회 완료: {}", storeData.getStoreName());
|
log.debug("매장 정보 조회 완료: {}", storeData.getStoreName());
|
||||||
|
|
||||||
// 날씨 정보 조회
|
// 2. 날씨 정보 조회
|
||||||
WeatherData weatherData = weatherDataProvider.getCurrentWeather(storeData.getLocation());
|
WeatherData weatherData = weatherDataProvider.getCurrentWeather(storeData.getLocation());
|
||||||
log.debug("날씨 정보 조회 완료: {} 도", weatherData.getTemperature());
|
log.debug("날씨 정보 조회 완료: 온도={}, 상태={}", weatherData.getTemperature(), weatherData.getCondition());
|
||||||
|
|
||||||
// AI를 사용하여 마케팅 팁 생성
|
// 3. AI 팁 생성
|
||||||
String tipContent = aiTipGenerator.generateTip(storeData, weatherData);
|
String aiGeneratedTip = aiTipGenerator.generateTip(storeData, weatherData, request.getAdditionalRequirement());
|
||||||
log.debug("AI 마케팅 팁 생성 완료");
|
log.debug("AI 팁 생성 완료: {}", aiGeneratedTip.substring(0, Math.min(50, aiGeneratedTip.length())));
|
||||||
|
|
||||||
// 마케팅 팁 도메인 객체 생성
|
// 4. 도메인 객체 생성 및 저장
|
||||||
MarketingTip marketingTip = MarketingTip.builder()
|
MarketingTip marketingTip = MarketingTip.builder()
|
||||||
.storeId(request.getStoreId())
|
.storeId(request.getStoreId())
|
||||||
.tipContent(tipContent)
|
.tipContent(aiGeneratedTip)
|
||||||
.weatherData(weatherData)
|
.weatherData(weatherData)
|
||||||
.storeData(storeData)
|
.storeData(storeData)
|
||||||
.createdAt(LocalDateTime.now())
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 마케팅 팁 저장
|
|
||||||
MarketingTip savedTip = marketingTipRepository.save(marketingTip);
|
MarketingTip savedTip = marketingTipRepository.save(marketingTip);
|
||||||
|
log.info("마케팅 팁 저장 완료: tipId={}", savedTip.getId().getValue());
|
||||||
|
|
||||||
return MarketingTipResponse.builder()
|
return convertToResponse(savedTip);
|
||||||
.tipId(savedTip.getId().getValue())
|
|
||||||
.tipContent(savedTip.getTipContent())
|
|
||||||
.createdAt(savedTip.getCreatedAt())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("마케팅 팁 생성 중 오류 발생", e);
|
log.error("마케팅 팁 생성 중 오류: storeId={}", request.getStoreId(), e);
|
||||||
throw new BusinessException(ErrorCode.RECOMMENDATION_FAILED);
|
throw new BusinessException(ErrorCode.AI_TIP_GENERATION_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Cacheable(value = "marketingTipHistory", key = "#storeId + '_' + #pageable.pageNumber + '_' + #pageable.pageSize")
|
||||||
|
public Page<MarketingTipResponse> getMarketingTipHistory(Long storeId, Pageable pageable) {
|
||||||
|
log.info("마케팅 팁 이력 조회: storeId={}", storeId);
|
||||||
|
|
||||||
|
Page<MarketingTip> tips = marketingTipRepository.findByStoreIdOrderByCreatedAtDesc(storeId, pageable);
|
||||||
|
|
||||||
|
return tips.map(this::convertToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public MarketingTipResponse getMarketingTip(Long tipId) {
|
||||||
|
log.info("마케팅 팁 상세 조회: tipId={}", tipId);
|
||||||
|
|
||||||
|
MarketingTip marketingTip = marketingTipRepository.findById(tipId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.MARKETING_TIP_NOT_FOUND));
|
||||||
|
|
||||||
|
return convertToResponse(marketingTip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarketingTipResponse convertToResponse(MarketingTip marketingTip) {
|
||||||
|
return MarketingTipResponse.builder()
|
||||||
|
.tipId(marketingTip.getId().getValue())
|
||||||
|
.storeId(marketingTip.getStoreId())
|
||||||
|
.storeName(marketingTip.getStoreData().getStoreName())
|
||||||
|
.businessType(marketingTip.getStoreData().getBusinessType())
|
||||||
|
.storeLocation(marketingTip.getStoreData().getLocation())
|
||||||
|
.createdAt(marketingTip.getCreatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MarketingTip toDomain() {
|
||||||
|
WeatherData weatherData = WeatherData.builder()
|
||||||
|
.temperature(this.weatherTemperature)
|
||||||
|
.condition(this.weatherCondition)
|
||||||
|
.humidity(this.weatherHumidity)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StoreData storeData = StoreData.builder()
|
||||||
|
.storeName(this.storeName)
|
||||||
|
.businessType(this.businessType)
|
||||||
|
.location(this.storeLocation)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return MarketingTip.builder()
|
||||||
|
.id(this.id != null ? TipId.of(this.id) : null)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.tipContent(this.tipContent)
|
||||||
|
.weatherData(weatherData)
|
||||||
|
.storeData(storeData)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.won.smarketing.recommend.application.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날씨 데이터 서비스 (Mock)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WeatherDataService {
|
||||||
|
|
||||||
|
@Cacheable(value = "weatherData", key = "#location")
|
||||||
|
public com.won.smarketing.recommend.service.MarketingTipService.WeatherInfo getCurrentWeather(String location) {
|
||||||
|
log.debug("날씨 정보 조회: location={}", location);
|
||||||
|
|
||||||
|
// Mock 데이터 반환
|
||||||
|
double temperature = 20.0 + (Math.random() * 15); // 20-35도
|
||||||
|
String[] conditions = {"맑음", "흐림", "비", "눈", "안개"};
|
||||||
|
String condition = conditions[(int) (Math.random() * conditions.length)];
|
||||||
|
double humidity = 50.0 + (Math.random() * 30); // 50-80%
|
||||||
|
|
||||||
|
return com.won.smarketing.recommend.service.MarketingTipService.WeatherInfo.builder()
|
||||||
|
.temperature(Math.round(temperature * 10) / 10.0)
|
||||||
|
.condition(condition)
|
||||||
|
.humidity(Math.round(humidity * 10) / 10.0)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,10 +2,12 @@ package com.won.smarketing.recommend.application.usecase;
|
|||||||
|
|
||||||
import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest;
|
import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest;
|
||||||
import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse;
|
import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 마케팅 팁 관련 Use Case 인터페이스
|
* 마케팅 팁 생성 유즈케이스 인터페이스
|
||||||
* AI 기반 마케팅 팁 생성 기능 정의
|
* 비즈니스 요구사항을 정의하는 애플리케이션 계층의 인터페이스
|
||||||
*/
|
*/
|
||||||
public interface MarketingTipUseCase {
|
public interface MarketingTipUseCase {
|
||||||
|
|
||||||
@ -13,7 +15,24 @@ public interface MarketingTipUseCase {
|
|||||||
* AI 마케팅 팁 생성
|
* AI 마케팅 팁 생성
|
||||||
*
|
*
|
||||||
* @param request 마케팅 팁 생성 요청
|
* @param request 마케팅 팁 생성 요청
|
||||||
* @return 생성된 마케팅 팁 응답
|
* @return 생성된 마케팅 팁 정보
|
||||||
*/
|
*/
|
||||||
MarketingTipResponse generateMarketingTips(MarketingTipRequest request);
|
MarketingTipResponse generateMarketingTips(MarketingTipRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마케팅 팁 이력 조회
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @param pageable 페이징 정보
|
||||||
|
* @return 마케팅 팁 이력 페이지
|
||||||
|
*/
|
||||||
|
Page<MarketingTipResponse> getMarketingTipHistory(Long storeId, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마케팅 팁 상세 조회
|
||||||
|
*
|
||||||
|
* @param tipId 팁 ID
|
||||||
|
* @return 마케팅 팁 상세 정보
|
||||||
|
*/
|
||||||
|
MarketingTipResponse getMarketingTip(Long tipId);
|
||||||
}
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.won.smarketing.recommend.config;
|
||||||
|
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캐시 설정
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableCaching
|
||||||
|
public class CacheConfig {
|
||||||
|
// 기본 Simple 캐시 사용
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.won.smarketing.recommend.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA 설정
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableJpaRepositories
|
||||||
|
public class JpaConfig {
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.won.smarketing.recommend.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import io.netty.channel.ChannelOption;
|
||||||
|
import io.netty.handler.timeout.ConnectTimeoutHandler;
|
||||||
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebClient 설정
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class WebClientConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebClient webClient() {
|
||||||
|
HttpClient httpClient = HttpClient.create()
|
||||||
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
|
||||||
|
.responseTimeout(Duration.ofMillis(5000))
|
||||||
|
.doOnConnected(conn -> conn
|
||||||
|
.addHandlerLast(new ConnectTimeoutHandler(5, TimeUnit.SECONDS)));
|
||||||
|
|
||||||
|
return WebClient.builder()
|
||||||
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||||
|
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package com.won.smarketing.recommend.domain.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비즈니스 인사이트 엔티티
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "business_insights")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public class BusinessInsight {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "insight_id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "store_id", nullable = false)
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Column(name = "insight_type", nullable = false, length = 50)
|
||||||
|
private String insightType;
|
||||||
|
|
||||||
|
@Column(name = "title", nullable = false, length = 200)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(name = "description", columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "metric_value")
|
||||||
|
private Double metricValue;
|
||||||
|
|
||||||
|
@Column(name = "recommendation", columnDefinition = "TEXT")
|
||||||
|
private String recommendation;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -1,58 +1,38 @@
|
|||||||
package com.won.smarketing.recommend.domain.model;
|
package com.won.smarketing.recommend.domain.model;
|
||||||
|
|
||||||
import lombok.*;
|
import com.won.smarketing.recommend.domain.model.StoreData;
|
||||||
|
import com.won.smarketing.recommend.domain.model.TipId;
|
||||||
|
import com.won.smarketing.recommend.domain.model.WeatherData;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 마케팅 팁 도메인 모델
|
* 마케팅 팁 도메인 모델
|
||||||
* AI가 생성한 마케팅 팁과 관련 정보를 관리
|
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class MarketingTip {
|
public class MarketingTip {
|
||||||
|
|
||||||
/**
|
|
||||||
* 마케팅 팁 고유 식별자
|
|
||||||
*/
|
|
||||||
private TipId id;
|
private TipId id;
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장 ID
|
|
||||||
*/
|
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
|
|
||||||
/**
|
|
||||||
* AI가 생성한 마케팅 팁 내용
|
|
||||||
*/
|
|
||||||
private String tipContent;
|
private String tipContent;
|
||||||
|
|
||||||
/**
|
|
||||||
* 팁 생성 시 참고한 날씨 데이터
|
|
||||||
*/
|
|
||||||
private WeatherData weatherData;
|
private WeatherData weatherData;
|
||||||
|
|
||||||
/**
|
|
||||||
* 팁 생성 시 참고한 매장 데이터
|
|
||||||
*/
|
|
||||||
private StoreData storeData;
|
private StoreData storeData;
|
||||||
|
|
||||||
/**
|
|
||||||
* 팁 생성 시각
|
|
||||||
*/
|
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
/**
|
public static MarketingTip create(Long storeId, String tipContent, WeatherData weatherData, StoreData storeData) {
|
||||||
* 팁 내용 업데이트
|
return MarketingTip.builder()
|
||||||
*
|
.storeId(storeId)
|
||||||
* @param newContent 새로운 팁 내용
|
.tipContent(tipContent)
|
||||||
*/
|
.weatherData(weatherData)
|
||||||
public void updateContent(String newContent) {
|
.storeData(storeData)
|
||||||
if (newContent == null || newContent.trim().isEmpty()) {
|
.createdAt(LocalDateTime.now())
|
||||||
throw new IllegalArgumentException("팁 내용은 비어있을 수 없습니다.");
|
.build();
|
||||||
}
|
|
||||||
this.tipContent = newContent.trim();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,66 +1,19 @@
|
|||||||
package com.won.smarketing.recommend.domain.model;
|
package com.won.smarketing.recommend.domain.model;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 데이터 값 객체
|
* 매장 데이터 값 객체
|
||||||
* 마케팅 팁 생성에 사용되는 매장 정보
|
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
@Builder
|
||||||
@EqualsAndHashCode
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class StoreData {
|
public class StoreData {
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장명
|
|
||||||
*/
|
|
||||||
private String storeName;
|
private String storeName;
|
||||||
|
|
||||||
/**
|
|
||||||
* 업종
|
|
||||||
*/
|
|
||||||
private String businessType;
|
private String businessType;
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장 위치 (주소)
|
|
||||||
*/
|
|
||||||
private String location;
|
private String location;
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장 데이터 유효성 검증
|
|
||||||
*
|
|
||||||
* @return 유효성 여부
|
|
||||||
*/
|
|
||||||
public boolean isValid() {
|
|
||||||
return storeName != null && !storeName.trim().isEmpty() &&
|
|
||||||
businessType != null && !businessType.trim().isEmpty() &&
|
|
||||||
location != null && !location.trim().isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 업종 카테고리 분류
|
|
||||||
*
|
|
||||||
* @return 업종 카테고리
|
|
||||||
*/
|
|
||||||
public String getBusinessCategory() {
|
|
||||||
if (businessType == null) {
|
|
||||||
return "기타";
|
|
||||||
}
|
|
||||||
|
|
||||||
String lowerCaseType = businessType.toLowerCase();
|
|
||||||
|
|
||||||
if (lowerCaseType.contains("카페") || lowerCaseType.contains("커피")) {
|
|
||||||
return "카페";
|
|
||||||
} else if (lowerCaseType.contains("식당") || lowerCaseType.contains("레스토랑")) {
|
|
||||||
return "음식점";
|
|
||||||
} else if (lowerCaseType.contains("베이커리") || lowerCaseType.contains("빵")) {
|
|
||||||
return "베이커리";
|
|
||||||
} else if (lowerCaseType.contains("치킨") || lowerCaseType.contains("피자")) {
|
|
||||||
return "패스트푸드";
|
|
||||||
} else {
|
|
||||||
return "기타";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,29 +1,21 @@
|
|||||||
package com.won.smarketing.recommend.domain.model;
|
package com.won.smarketing.recommend.domain.model;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 마케팅 팁 식별자 값 객체
|
* 팁 ID 값 객체
|
||||||
* 마케팅 팁의 고유 식별자를 나타내는 도메인 객체
|
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
||||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class TipId {
|
public class TipId {
|
||||||
|
|
||||||
private Long value;
|
private Long value;
|
||||||
|
|
||||||
/**
|
|
||||||
* TipId 생성 팩토리 메서드
|
|
||||||
*
|
|
||||||
* @param value 식별자 값
|
|
||||||
* @return TipId 인스턴스
|
|
||||||
*/
|
|
||||||
public static TipId of(Long value) {
|
public static TipId of(Long value) {
|
||||||
if (value == null || value <= 0) {
|
|
||||||
throw new IllegalArgumentException("TipId는 양수여야 합니다.");
|
|
||||||
}
|
|
||||||
return new TipId(value);
|
return new TipId(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,66 +1,19 @@
|
|||||||
package com.won.smarketing.recommend.domain.model;
|
package com.won.smarketing.recommend.domain.model;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 날씨 데이터 값 객체
|
* 날씨 데이터 값 객체
|
||||||
* 마케팅 팁 생성에 사용되는 날씨 정보
|
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
@Builder
|
||||||
@EqualsAndHashCode
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class WeatherData {
|
public class WeatherData {
|
||||||
|
|
||||||
/**
|
|
||||||
* 온도 (섭씨)
|
|
||||||
*/
|
|
||||||
private Double temperature;
|
private Double temperature;
|
||||||
|
|
||||||
/**
|
|
||||||
* 날씨 상태 (맑음, 흐림, 비, 눈 등)
|
|
||||||
*/
|
|
||||||
private String condition;
|
private String condition;
|
||||||
|
|
||||||
/**
|
|
||||||
* 습도 (%)
|
|
||||||
*/
|
|
||||||
private Double humidity;
|
private Double humidity;
|
||||||
|
|
||||||
/**
|
|
||||||
* 날씨 데이터 유효성 검증
|
|
||||||
*
|
|
||||||
* @return 유효성 여부
|
|
||||||
*/
|
|
||||||
public boolean isValid() {
|
|
||||||
return temperature != null &&
|
|
||||||
condition != null && !condition.trim().isEmpty() &&
|
|
||||||
humidity != null && humidity >= 0 && humidity <= 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 온도 기반 날씨 상태 설명
|
|
||||||
*
|
|
||||||
* @return 날씨 상태 설명
|
|
||||||
*/
|
|
||||||
public String getTemperatureDescription() {
|
|
||||||
if (temperature == null) {
|
|
||||||
return "알 수 없음";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (temperature >= 30) {
|
|
||||||
return "매우 더움";
|
|
||||||
} else if (temperature >= 25) {
|
|
||||||
return "더움";
|
|
||||||
} else if (temperature >= 20) {
|
|
||||||
return "따뜻함";
|
|
||||||
} else if (temperature >= 10) {
|
|
||||||
return "선선함";
|
|
||||||
} else if (temperature >= 0) {
|
|
||||||
return "춥다";
|
|
||||||
} else {
|
|
||||||
return "매우 춥다";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.won.smarketing.recommend.domain.repository;
|
||||||
|
|
||||||
|
import com.won.smarketing.recommend.domain.model.BusinessInsight;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface BusinessInsightRepository extends JpaRepository<BusinessInsight, Long> {
|
||||||
|
|
||||||
|
List<BusinessInsight> findByStoreIdOrderByCreatedAtDesc(Long storeId);
|
||||||
|
|
||||||
|
List<BusinessInsight> findByInsightTypeAndStoreId(String insightType, Long storeId);
|
||||||
|
}
|
||||||
@ -1,56 +1,19 @@
|
|||||||
package com.won.smarketing.recommend.domain.repository;
|
package com.won.smarketing.recommend.domain.repository;
|
||||||
|
|
||||||
import com.won.smarketing.recommend.domain.model.MarketingTip;
|
import com.won.smarketing.recommend.domain.model.MarketingTip;
|
||||||
import com.won.smarketing.recommend.domain.model.TipId;
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 마케팅 팁 저장소 인터페이스
|
* 마케팅 팁 레포지토리 인터페이스
|
||||||
* 마케팅 팁 도메인의 데이터 접근 추상화
|
|
||||||
*/
|
*/
|
||||||
public interface MarketingTipRepository {
|
public interface MarketingTipRepository {
|
||||||
|
|
||||||
/**
|
|
||||||
* 마케팅 팁 저장
|
|
||||||
*
|
|
||||||
* @param marketingTip 저장할 마케팅 팁
|
|
||||||
* @return 저장된 마케팅 팁
|
|
||||||
*/
|
|
||||||
MarketingTip save(MarketingTip marketingTip);
|
MarketingTip save(MarketingTip marketingTip);
|
||||||
|
|
||||||
/**
|
Optional<MarketingTip> findById(Long tipId);
|
||||||
* 마케팅 팁 ID로 조회
|
|
||||||
*
|
|
||||||
* @param id 마케팅 팁 ID
|
|
||||||
* @return 마케팅 팁 (Optional)
|
|
||||||
*/
|
|
||||||
Optional<MarketingTip> findById(TipId id);
|
|
||||||
|
|
||||||
/**
|
Page<MarketingTip> findByStoreIdOrderByCreatedAtDesc(Long storeId, Pageable pageable);
|
||||||
* 매장별 마케팅 팁 목록 조회
|
|
||||||
*
|
|
||||||
* @param storeId 매장 ID
|
|
||||||
* @return 마케팅 팁 목록
|
|
||||||
*/
|
|
||||||
List<MarketingTip> findByStoreId(Long storeId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 특정 기간 내 생성된 마케팅 팁 조회
|
|
||||||
*
|
|
||||||
* @param storeId 매장 ID
|
|
||||||
* @param startDate 시작 시각
|
|
||||||
* @param endDate 종료 시각
|
|
||||||
* @return 마케팅 팁 목록
|
|
||||||
*/
|
|
||||||
List<MarketingTip> findByStoreIdAndCreatedAtBetween(Long storeId, LocalDateTime startDate, LocalDateTime endDate);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 마케팅 팁 삭제
|
|
||||||
*
|
|
||||||
* @param id 삭제할 마케팅 팁 ID
|
|
||||||
*/
|
|
||||||
void deleteById(TipId id);
|
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ import com.won.smarketing.recommend.domain.model.StoreData;
|
|||||||
public interface StoreDataProvider {
|
public interface StoreDataProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 ID로 매장 데이터 조회
|
* 매장 정보 조회
|
||||||
*
|
*
|
||||||
* @param storeId 매장 ID
|
* @param storeId 매장 ID
|
||||||
* @return 매장 데이터
|
* @return 매장 데이터
|
||||||
|
|||||||
@ -0,0 +1,137 @@
|
|||||||
|
//import com.won.smarketing.recommend.domain.model.StoreData;
|
||||||
|
//import com.won.smarketing.recommend.domain.model.WeatherData;
|
||||||
|
//import com.won.smarketing.recommend.domain.service.AiTipGenerator;
|
||||||
|
//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.util.Map;
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * 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(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||||
|
// try {
|
||||||
|
// log.debug("Python AI 서비스 호출: store={}, weather={}도",
|
||||||
|
// storeData.getStoreName(), weatherData.getTemperature());
|
||||||
|
//
|
||||||
|
// // Python AI 서비스 사용 가능 여부 확인
|
||||||
|
// if (isPythonServiceAvailable()) {
|
||||||
|
// return callPythonAiService(storeData, weatherData, additionalRequirement);
|
||||||
|
// } else {
|
||||||
|
// log.warn("Python AI 서비스 사용 불가, Fallback 처리");
|
||||||
|
// return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// log.error("Python AI 서비스 호출 실패, Fallback 처리: {}", e.getMessage());
|
||||||
|
// return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private boolean isPythonServiceAvailable() {
|
||||||
|
// return !pythonAiServiceApiKey.equals("dummy-key");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String callPythonAiService(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||||
|
// try {
|
||||||
|
// Map<String, Object> requestData = Map.of(
|
||||||
|
// "store_name", storeData.getStoreName(),
|
||||||
|
// "business_type", storeData.getBusinessType(),
|
||||||
|
// "location", storeData.getLocation(),
|
||||||
|
// "temperature", weatherData.getTemperature(),
|
||||||
|
// "weather_condition", weatherData.getCondition(),
|
||||||
|
// "humidity", weatherData.getHumidity(),
|
||||||
|
// "additional_requirement", additionalRequirement != null ? additionalRequirement : ""
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// 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()) {
|
||||||
|
// return response.getTip();
|
||||||
|
// }
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// log.error("Python AI 서비스 실제 호출 실패: {}", e.getMessage());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String createFallbackTip(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||||
|
// String businessType = storeData.getBusinessType();
|
||||||
|
// double temperature = weatherData.getTemperature();
|
||||||
|
// String condition = weatherData.getCondition();
|
||||||
|
// String storeName = storeData.getStoreName();
|
||||||
|
//
|
||||||
|
// // 추가 요청사항이 있는 경우 우선 반영
|
||||||
|
// if (additionalRequirement != null && !additionalRequirement.trim().isEmpty()) {
|
||||||
|
// return String.format("%s에서 %s를 고려한 특별한 서비스로 고객들을 맞이해보세요!",
|
||||||
|
// storeName, additionalRequirement);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 날씨와 업종 기반 규칙
|
||||||
|
// if (temperature > 25) {
|
||||||
|
// if (businessType.contains("카페")) {
|
||||||
|
// return String.format("더운 날씨(%.1f도)에는 시원한 아이스 음료와 디저트로 고객들을 시원하게 만족시켜보세요!", temperature);
|
||||||
|
// } else {
|
||||||
|
// return "더운 여름날, 시원한 음료나 냉면으로 고객들에게 청량감을 선사해보세요!";
|
||||||
|
// }
|
||||||
|
// } else if (temperature < 10) {
|
||||||
|
// if (businessType.contains("카페")) {
|
||||||
|
// return String.format("추운 날씨(%.1f도)에는 따뜻한 음료와 베이커리로 고객들에게 따뜻함을 전해보세요!", temperature);
|
||||||
|
// } else {
|
||||||
|
// return "추운 겨울날, 따뜻한 국물 요리로 고객들의 몸과 마음을 따뜻하게 해보세요!";
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (condition.contains("비")) {
|
||||||
|
// return "비 오는 날에는 따뜻한 음료와 분위기로 고객들의 마음을 따뜻하게 해보세요!";
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 기본 팁
|
||||||
|
// return String.format("%s에서 오늘(%.1f도, %s) 같은 날씨에 어울리는 특별한 서비스로 고객들을 맞이해보세요!",
|
||||||
|
// storeName, temperature, condition);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private static class PythonAiResponse {
|
||||||
|
// private String tip;
|
||||||
|
// private String status;
|
||||||
|
// private String message;
|
||||||
|
//
|
||||||
|
// public String getTip() { return tip; }
|
||||||
|
// public void setTip(String tip) { this.tip = tip; }
|
||||||
|
// public String getStatus() { return status; }
|
||||||
|
// public void setStatus(String status) { this.status = status; }
|
||||||
|
// public String getMessage() { return message; }
|
||||||
|
// public void setMessage(String message) { this.message = message; }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@ -151,7 +151,7 @@ public class ClaudeAiTipGenerator implements AiTipGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 업종별 기본 팁
|
// 업종별 기본 팁
|
||||||
String businessCategory = storeData.getBusinessCategory();
|
String businessCategory = storeData.getBusinessType();
|
||||||
switch (businessCategory) {
|
switch (businessCategory) {
|
||||||
case "카페":
|
case "카페":
|
||||||
tip.append("인스타그램용 예쁜 음료 사진을 올려보세요.");
|
tip.append("인스타그램용 예쁜 음료 사진을 올려보세요.");
|
||||||
|
|||||||
@ -0,0 +1,137 @@
|
|||||||
|
import com.won.smarketing.recommend.domain.model.StoreData;
|
||||||
|
import com.won.smarketing.recommend.domain.model.WeatherData;
|
||||||
|
import com.won.smarketing.recommend.domain.service.AiTipGenerator;
|
||||||
|
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.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||||
|
try {
|
||||||
|
log.debug("Python AI 서비스 호출: store={}, weather={}도",
|
||||||
|
storeData.getStoreName(), weatherData.getTemperature());
|
||||||
|
|
||||||
|
// Python AI 서비스 사용 가능 여부 확인
|
||||||
|
if (isPythonServiceAvailable()) {
|
||||||
|
return callPythonAiService(storeData, weatherData, additionalRequirement);
|
||||||
|
} else {
|
||||||
|
log.warn("Python AI 서비스 사용 불가, Fallback 처리");
|
||||||
|
return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Python AI 서비스 호출 실패, Fallback 처리: {}", e.getMessage());
|
||||||
|
return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPythonServiceAvailable() {
|
||||||
|
return !pythonAiServiceApiKey.equals("dummy-key");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String callPythonAiService(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> requestData = Map.of(
|
||||||
|
"store_name", storeData.getStoreName(),
|
||||||
|
"business_type", storeData.getBusinessType(),
|
||||||
|
"location", storeData.getLocation(),
|
||||||
|
"temperature", weatherData.getTemperature(),
|
||||||
|
"weather_condition", weatherData.getCondition(),
|
||||||
|
"humidity", weatherData.getHumidity(),
|
||||||
|
"additional_requirement", additionalRequirement != null ? additionalRequirement : ""
|
||||||
|
);
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
return response.getTip();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Python AI 서비스 실제 호출 실패: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createFallbackTip(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||||
|
String businessType = storeData.getBusinessType();
|
||||||
|
double temperature = weatherData.getTemperature();
|
||||||
|
String condition = weatherData.getCondition();
|
||||||
|
String storeName = storeData.getStoreName();
|
||||||
|
|
||||||
|
// 추가 요청사항이 있는 경우 우선 반영
|
||||||
|
if (additionalRequirement != null && !additionalRequirement.trim().isEmpty()) {
|
||||||
|
return String.format("%s에서 %s를 고려한 특별한 서비스로 고객들을 맞이해보세요!",
|
||||||
|
storeName, additionalRequirement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날씨와 업종 기반 규칙
|
||||||
|
if (temperature > 25) {
|
||||||
|
if (businessType.contains("카페")) {
|
||||||
|
return String.format("더운 날씨(%.1f도)에는 시원한 아이스 음료와 디저트로 고객들을 시원하게 만족시켜보세요!", temperature);
|
||||||
|
} else {
|
||||||
|
return "더운 여름날, 시원한 음료나 냉면으로 고객들에게 청량감을 선사해보세요!";
|
||||||
|
}
|
||||||
|
} else if (temperature < 10) {
|
||||||
|
if (businessType.contains("카페")) {
|
||||||
|
return String.format("추운 날씨(%.1f도)에는 따뜻한 음료와 베이커리로 고객들에게 따뜻함을 전해보세요!", temperature);
|
||||||
|
} else {
|
||||||
|
return "추운 겨울날, 따뜻한 국물 요리로 고객들의 몸과 마음을 따뜻하게 해보세요!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition.contains("비")) {
|
||||||
|
return "비 오는 날에는 따뜻한 음료와 분위기로 고객들의 마음을 따뜻하게 해보세요!";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 팁
|
||||||
|
return String.format("%s에서 오늘(%.1f도, %s) 같은 날씨에 어울리는 특별한 서비스로 고객들을 맞이해보세요!",
|
||||||
|
storeName, temperature, condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PythonAiResponse {
|
||||||
|
private String tip;
|
||||||
|
private String status;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public String getTip() { return tip; }
|
||||||
|
public void setTip(String tip) { this.tip = tip; }
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
public String getMessage() { return message; }
|
||||||
|
public void setMessage(String message) { this.message = message; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,16 +7,15 @@ import com.won.smarketing.recommend.domain.service.StoreDataProvider;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 API 데이터 제공자 구현체
|
* 매장 API 데이터 제공자 구현체
|
||||||
* 외부 매장 서비스 API를 통해 매장 정보 조회
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -28,76 +27,88 @@ public class StoreApiDataProvider implements StoreDataProvider {
|
|||||||
@Value("${external.store-service.base-url}")
|
@Value("${external.store-service.base-url}")
|
||||||
private String storeServiceBaseUrl;
|
private String storeServiceBaseUrl;
|
||||||
|
|
||||||
/**
|
@Value("${external.store-service.timeout}")
|
||||||
* 매장 ID로 매장 데이터 조회
|
private int timeout;
|
||||||
*
|
|
||||||
* @param storeId 매장 ID
|
|
||||||
* @return 매장 데이터
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
|
@Cacheable(value = "storeData", key = "#storeId")
|
||||||
public StoreData getStoreData(Long storeId) {
|
public StoreData getStoreData(Long storeId) {
|
||||||
try {
|
try {
|
||||||
log.debug("매장 정보 조회 시작: storeId={}", storeId);
|
log.debug("매장 정보 조회 시도: storeId={}", storeId);
|
||||||
|
|
||||||
|
// 외부 서비스 연결 시도, 실패 시 Mock 데이터 반환
|
||||||
|
if (isStoreServiceAvailable()) {
|
||||||
|
return callStoreService(storeId);
|
||||||
|
} else {
|
||||||
|
log.warn("매장 서비스 연결 불가, Mock 데이터 반환: storeId={}", storeId);
|
||||||
|
return createMockStoreData(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("매장 정보 조회 실패, Mock 데이터 반환: storeId={}", storeId, e);
|
||||||
|
return createMockStoreData(storeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isStoreServiceAvailable() {
|
||||||
|
return !storeServiceBaseUrl.equals("http://localhost:8082");
|
||||||
|
}
|
||||||
|
|
||||||
|
private StoreData callStoreService(Long storeId) {
|
||||||
|
try {
|
||||||
StoreApiResponse response = webClient
|
StoreApiResponse response = webClient
|
||||||
.get()
|
.get()
|
||||||
.uri(storeServiceBaseUrl + "/api/store?storeId=" + storeId)
|
.uri(storeServiceBaseUrl + "/api/store/" + storeId)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(StoreApiResponse.class)
|
.bodyToMono(StoreApiResponse.class)
|
||||||
.timeout(Duration.ofSeconds(10))
|
.timeout(Duration.ofMillis(timeout))
|
||||||
.block();
|
.block();
|
||||||
|
|
||||||
if (response == null || response.getData() == null) {
|
if (response != null && response.getData() != null) {
|
||||||
|
StoreApiResponse.StoreInfo storeInfo = response.getData();
|
||||||
|
return StoreData.builder()
|
||||||
|
.storeName(storeInfo.getStoreName())
|
||||||
|
.businessType(storeInfo.getBusinessType())
|
||||||
|
.location(storeInfo.getAddress())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
} catch (WebClientResponseException e) {
|
||||||
|
if (e.getStatusCode().value() == 404) {
|
||||||
throw new BusinessException(ErrorCode.STORE_NOT_FOUND);
|
throw new BusinessException(ErrorCode.STORE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
log.error("매장 서비스 호출 실패: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
StoreApiData storeApiData = response.getData();
|
return createMockStoreData(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
StoreData storeData = StoreData.builder()
|
private StoreData createMockStoreData(Long storeId) {
|
||||||
.storeName(storeApiData.getStoreName())
|
return StoreData.builder()
|
||||||
.businessType(storeApiData.getBusinessType())
|
.storeName("테스트 카페 " + storeId)
|
||||||
.location(storeApiData.getAddress())
|
.businessType("카페")
|
||||||
|
.location("서울시 강남구")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
log.debug("매장 정보 조회 완료: {}", storeData.getStoreName());
|
|
||||||
return storeData;
|
|
||||||
|
|
||||||
} catch (WebClientResponseException e) {
|
|
||||||
log.error("매장 서비스 API 호출 실패: storeId={}, status={}", storeId, e.getStatusCode(), e);
|
|
||||||
throw new BusinessException(ErrorCode.EXTERNAL_API_ERROR);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("매장 정보 조회 중 오류 발생: storeId={}", storeId, e);
|
|
||||||
throw new BusinessException(ErrorCode.EXTERNAL_API_ERROR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장 API 응답 DTO
|
|
||||||
*/
|
|
||||||
private static class StoreApiResponse {
|
private static class StoreApiResponse {
|
||||||
private int status;
|
private int status;
|
||||||
private String message;
|
private String message;
|
||||||
private StoreApiData data;
|
private StoreInfo data;
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public int getStatus() { return status; }
|
public int getStatus() { return status; }
|
||||||
public void setStatus(int status) { this.status = status; }
|
public void setStatus(int status) { this.status = status; }
|
||||||
public String getMessage() { return message; }
|
public String getMessage() { return message; }
|
||||||
public void setMessage(String message) { this.message = message; }
|
public void setMessage(String message) { this.message = message; }
|
||||||
public StoreApiData getData() { return data; }
|
public StoreInfo getData() { return data; }
|
||||||
public void setData(StoreApiData data) { this.data = data; }
|
public void setData(StoreInfo data) { this.data = data; }
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
static class StoreInfo {
|
||||||
* 매장 API 데이터 DTO
|
|
||||||
*/
|
|
||||||
private static class StoreApiData {
|
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
private String storeName;
|
private String storeName;
|
||||||
private String businessType;
|
private String businessType;
|
||||||
private String address;
|
private String address;
|
||||||
|
private String phoneNumber;
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public Long getStoreId() { return storeId; }
|
public Long getStoreId() { return storeId; }
|
||||||
public void setStoreId(Long storeId) { this.storeId = storeId; }
|
public void setStoreId(Long storeId) { this.storeId = storeId; }
|
||||||
public String getStoreName() { return storeName; }
|
public String getStoreName() { return storeName; }
|
||||||
@ -106,5 +117,8 @@ public class StoreApiDataProvider implements StoreDataProvider {
|
|||||||
public void setBusinessType(String businessType) { this.businessType = businessType; }
|
public void setBusinessType(String businessType) { this.businessType = businessType; }
|
||||||
public String getAddress() { return address; }
|
public String getAddress() { return address; }
|
||||||
public void setAddress(String address) { this.address = address; }
|
public void setAddress(String address) { this.address = address; }
|
||||||
|
public String getPhoneNumber() { return phoneNumber; }
|
||||||
|
public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,22 +1,18 @@
|
|||||||
package com.won.smarketing.recommend.infrastructure.external;
|
package com.won.smarketing.recommend.infrastructure.external;
|
||||||
|
|
||||||
import com.won.smarketing.common.exception.BusinessException;
|
|
||||||
import com.won.smarketing.common.exception.ErrorCode;
|
|
||||||
import com.won.smarketing.recommend.domain.model.WeatherData;
|
import com.won.smarketing.recommend.domain.model.WeatherData;
|
||||||
import com.won.smarketing.recommend.domain.service.WeatherDataProvider;
|
import com.won.smarketing.recommend.domain.service.WeatherDataProvider;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 날씨 API 데이터 제공자 구현체
|
* 날씨 API 데이터 제공자 구현체
|
||||||
* 외부 날씨 API를 통해 날씨 정보 조회
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -28,128 +24,45 @@ public class WeatherApiDataProvider implements WeatherDataProvider {
|
|||||||
@Value("${external.weather-api.api-key}")
|
@Value("${external.weather-api.api-key}")
|
||||||
private String weatherApiKey;
|
private String weatherApiKey;
|
||||||
|
|
||||||
@Value("${external.weather-api.base-url}")
|
@Value("${external.weather-api.timeout}")
|
||||||
private String weatherApiBaseUrl;
|
private int timeout;
|
||||||
|
|
||||||
/**
|
|
||||||
* 특정 위치의 현재 날씨 정보 조회
|
|
||||||
*
|
|
||||||
* @param location 위치 (주소)
|
|
||||||
* @return 날씨 데이터
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public WeatherApiResponse getCurrentWeather(String location) {
|
@Cacheable(value = "weatherData", key = "#location")
|
||||||
|
public WeatherData getCurrentWeather(String location) {
|
||||||
try {
|
try {
|
||||||
log.debug("날씨 정보 조회 시작: location={}", location);
|
log.debug("날씨 정보 조회: location={}", location);
|
||||||
|
|
||||||
// 한국 주요 도시로 단순화
|
// 개발 환경에서는 Mock 데이터 반환
|
||||||
String city = extractCity(location);
|
if (weatherApiKey.equals("dummy-key")) {
|
||||||
|
return createMockWeatherData(location);
|
||||||
WeatherApiResponse response = webClient
|
|
||||||
.get()
|
|
||||||
.uri(uriBuilder -> uriBuilder
|
|
||||||
.scheme("https")
|
|
||||||
.host("api.openweathermap.org")
|
|
||||||
.path("/data/2.5/weather")
|
|
||||||
.queryParam("q", city + ",KR")
|
|
||||||
.queryParam("appid", weatherApiKey)
|
|
||||||
.queryParam("units", "metric")
|
|
||||||
.queryParam("lang", "kr")
|
|
||||||
.build())
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(WeatherApiResponse.class)
|
|
||||||
.timeout(Duration.ofSeconds(10))
|
|
||||||
.onErrorReturn(createDefaultWeatherData()) // 오류 시 기본값 반환
|
|
||||||
.block();
|
|
||||||
|
|
||||||
if (response == null) {
|
|
||||||
return createDefaultWeatherData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WeatherData weatherData = WeatherData.builder()
|
// 실제 날씨 API 호출 (향후 구현)
|
||||||
.temperature(response.getMain().getTemp())
|
return callWeatherApi(location);
|
||||||
.condition(response.getWeather()[0].getDescription())
|
|
||||||
.humidity(response.getMain().getHumidity())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
log.debug("날씨 정보 조회 완료: {}도, {}", weatherData.getTemperature(), weatherData.getCondition());
|
|
||||||
return weatherData;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("날씨 정보 조회 실패, 기본값 사용: location={}", location, e);
|
log.warn("날씨 정보 조회 실패, Mock 데이터 사용: location={}", location, e);
|
||||||
return createDefaultWeatherData();
|
return createMockWeatherData(location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private WeatherData callWeatherApi(String location) {
|
||||||
* 주소에서 도시명 추출
|
// 실제 OpenWeatherMap API 호출 로직 (향후 구현)
|
||||||
*
|
log.info("실제 날씨 API 호출: {}", location);
|
||||||
* @param location 전체 주소
|
return createMockWeatherData(location);
|
||||||
* @return 도시명
|
|
||||||
*/
|
|
||||||
private String extractCity(String location) {
|
|
||||||
if (location == null || location.trim().isEmpty()) {
|
|
||||||
return "Seoul";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 서울, 부산, 대구, 인천, 광주, 대전, 울산 등 주요 도시 추출
|
private WeatherData createMockWeatherData(String location) {
|
||||||
String[] cities = {"서울", "부산", "대구", "인천", "광주", "대전", "울산", "세종", "수원", "창원"};
|
double temperature = 20.0 + (Math.random() * 15); // 20-35도
|
||||||
|
String[] conditions = {"맑음", "흐림", "비", "눈", "안개"};
|
||||||
|
String condition = conditions[(int) (Math.random() * conditions.length)];
|
||||||
|
double humidity = 50.0 + (Math.random() * 30); // 50-80%
|
||||||
|
|
||||||
for (String city : cities) {
|
return WeatherData.builder()
|
||||||
if (location.contains(city)) {
|
.temperature(Math.round(temperature * 10) / 10.0)
|
||||||
return city;
|
.condition(condition)
|
||||||
}
|
.humidity(Math.round(humidity * 10) / 10.0)
|
||||||
}
|
.build();
|
||||||
|
|
||||||
return "Seoul"; // 기본값
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 기본 날씨 데이터 생성 (API 호출 실패 시 사용)
|
|
||||||
*
|
|
||||||
* @return 기본 날씨 데이터
|
|
||||||
*/
|
|
||||||
private WeatherApiResponse createDefaultWeatherData() {
|
|
||||||
WeatherApiResponse response = new WeatherApiResponse();
|
|
||||||
response.setMain(new WeatherApiResponse.Main());
|
|
||||||
response.getMain().setTemp(20.0); // 기본 온도 20도
|
|
||||||
response.getMain().setHumidity(60.0); // 기본 습도 60%
|
|
||||||
|
|
||||||
WeatherApiResponse.Weather[] weather = new WeatherApiResponse.Weather[1];
|
|
||||||
weather[0] = new WeatherApiResponse.Weather();
|
|
||||||
weather[0].setDescription("맑음");
|
|
||||||
response.setWeather(weather);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 날씨 API 응답 DTO
|
|
||||||
*/
|
|
||||||
private static class WeatherApiResponse {
|
|
||||||
private Main main;
|
|
||||||
private Weather[] weather;
|
|
||||||
|
|
||||||
public Main getMain() { return main; }
|
|
||||||
public void setMain(Main main) { this.main = main; }
|
|
||||||
public Weather[] getWeather() { return weather; }
|
|
||||||
public void setWeather(Weather[] weather) { this.weather = weather; }
|
|
||||||
|
|
||||||
static class Main {
|
|
||||||
private Double temp;
|
|
||||||
private Double humidity;
|
|
||||||
|
|
||||||
public Double getTemp() { return temp; }
|
|
||||||
public void setTemp(Double temp) { this.temp = temp; }
|
|
||||||
public Double getHumidity() { return humidity; }
|
|
||||||
public void setHumidity(Double humidity) { this.humidity = humidity; }
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Weather {
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
public String getDescription() { return description; }
|
|
||||||
public void setDescription(String description) { this.description = description; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import com.won.smarketing.recommend.domain.model.MarketingTip;
|
||||||
|
import com.won.smarketing.recommend.domain.repository.MarketingTipRepository;
|
||||||
|
import com.won.smarketing.recommend.infrastructure.persistence.MarketingTipJpaRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA 마케팅 팁 레포지토리 구현체
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JpaMarketingTipRepository implements MarketingTipRepository {
|
||||||
|
|
||||||
|
private final MarketingTipJpaRepository jpaRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MarketingTip save(MarketingTip marketingTip) {
|
||||||
|
com.won.smarketing.recommend.entity.MarketingTipEntity entity = MarketingTipEntity.fromDomain(marketingTip);
|
||||||
|
com.won.smarketing.recommend.entity.MarketingTipEntity savedEntity = jpaRepository.save(entity);
|
||||||
|
return savedEntity.toDomain();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<MarketingTip> findById(Long tipId) {
|
||||||
|
return jpaRepository.findById(tipId)
|
||||||
|
.map(MarketingTipEntity::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<MarketingTip> findByStoreIdOrderByCreatedAtDesc(Long storeId, Pageable pageable) {
|
||||||
|
return jpaRepository.findByStoreIdOrderByCreatedAtDesc(storeId, pageable)
|
||||||
|
.map(MarketingTipEntity::toDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.won.smarketing.recommend.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마케팅 팁 JPA 엔티티
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "marketing_tips")
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MarketingTipEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "store_id", nullable = false)
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Column(name = "tip_content", columnDefinition = "TEXT", nullable = false)
|
||||||
|
private String tipContent;
|
||||||
|
|
||||||
|
// WeatherData 임베디드
|
||||||
|
@Column(name = "weather_temperature")
|
||||||
|
private Double weatherTemperature;
|
||||||
|
|
||||||
|
@Column(name = "weather_condition", length = 100)
|
||||||
|
private String weatherCondition;
|
||||||
|
|
||||||
|
@Column(name = "weather_humidity")
|
||||||
|
private Double weatherHumidity;
|
||||||
|
|
||||||
|
// StoreData 임베디드
|
||||||
|
@Column(name = "store_name", length = 200)
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Column(name = "business_type", length = 100)
|
||||||
|
private String businessType;
|
||||||
|
|
||||||
|
@Column(name = "store_location", length = 500)
|
||||||
|
private String storeLocation;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마케팅 팁 JPA 레포지토리
|
||||||
|
*/
|
||||||
|
public interface MarketingTipJpaRepository extends JpaRepository<com.won.smarketing.recommend.entity.MarketingTipEntity, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT m FROM MarketingTipEntity m WHERE m.storeId = :storeId ORDER BY m.createdAt DESC")
|
||||||
|
Page<com.won.smarketing.recommend.entity.MarketingTipEntity> findByStoreIdOrderByCreatedAtDesc(@Param("storeId") Long storeId, Pageable pageable);
|
||||||
|
}
|
||||||
@ -5,35 +5,73 @@ import com.won.smarketing.recommend.application.usecase.MarketingTipUseCase;
|
|||||||
import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest;
|
import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest;
|
||||||
import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse;
|
import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 마케팅 추천을 위한 REST API 컨트롤러
|
* AI 마케팅 추천 컨트롤러
|
||||||
* AI 기반 마케팅 팁 생성 기능 제공
|
|
||||||
*/
|
*/
|
||||||
@Tag(name = "AI 마케팅 추천", description = "AI 기반 맞춤형 마케팅 추천 API")
|
@Tag(name = "AI 추천", description = "AI 기반 마케팅 팁 추천 API")
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/recommendation")
|
@RequestMapping("/api/recommendations")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RecommendationController {
|
public class RecommendationController {
|
||||||
|
|
||||||
private final MarketingTipUseCase marketingTipUseCase;
|
private final MarketingTipUseCase marketingTipUseCase;
|
||||||
|
|
||||||
/**
|
@Operation(
|
||||||
* AI 마케팅 팁 생성
|
summary = "AI 마케팅 팁 생성",
|
||||||
*
|
description = "매장 정보와 환경 데이터를 기반으로 AI 마케팅 팁을 생성합니다."
|
||||||
* @param request 마케팅 팁 생성 요청
|
)
|
||||||
* @return 생성된 마케팅 팁
|
|
||||||
*/
|
|
||||||
@Operation(summary = "AI 마케팅 팁 생성", description = "매장 특성과 환경 정보를 바탕으로 AI 마케팅 팁을 생성합니다.")
|
|
||||||
@PostMapping("/marketing-tips")
|
@PostMapping("/marketing-tips")
|
||||||
public ResponseEntity<ApiResponse<MarketingTipResponse>> generateMarketingTips(@Valid @RequestBody MarketingTipRequest request) {
|
public ResponseEntity<ApiResponse<MarketingTipResponse>> generateMarketingTips(
|
||||||
|
@Parameter(description = "마케팅 팁 생성 요청") @Valid @RequestBody MarketingTipRequest request) {
|
||||||
|
|
||||||
|
log.info("AI 마케팅 팁 생성 요청: storeId={}", request.getStoreId());
|
||||||
|
|
||||||
MarketingTipResponse response = marketingTipUseCase.generateMarketingTips(request);
|
MarketingTipResponse response = marketingTipUseCase.generateMarketingTips(request);
|
||||||
return ResponseEntity.ok(ApiResponse.success(response, "AI 마케팅 팁이 성공적으로 생성되었습니다."));
|
|
||||||
|
log.info("AI 마케팅 팁 생성 완료: tipId={}", response.getTipId());
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "마케팅 팁 이력 조회",
|
||||||
|
description = "특정 매장의 마케팅 팁 생성 이력을 조회합니다."
|
||||||
|
)
|
||||||
|
@GetMapping("/marketing-tips")
|
||||||
|
public ResponseEntity<ApiResponse<Page<MarketingTipResponse>>> getMarketingTipHistory(
|
||||||
|
@Parameter(description = "매장 ID") @RequestParam Long storeId,
|
||||||
|
Pageable pageable) {
|
||||||
|
|
||||||
|
log.info("마케팅 팁 이력 조회: storeId={}, page={}", storeId, pageable.getPageNumber());
|
||||||
|
|
||||||
|
Page<MarketingTipResponse> response = marketingTipUseCase.getMarketingTipHistory(storeId, pageable);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "마케팅 팁 상세 조회",
|
||||||
|
description = "특정 마케팅 팁의 상세 정보를 조회합니다."
|
||||||
|
)
|
||||||
|
@GetMapping("/marketing-tips/{tipId}")
|
||||||
|
public ResponseEntity<ApiResponse<MarketingTipResponse>> getMarketingTip(
|
||||||
|
@Parameter(description = "팁 ID") @PathVariable Long tipId) {
|
||||||
|
|
||||||
|
log.info("마케팅 팁 상세 조회: tipId={}", tipId);
|
||||||
|
|
||||||
|
MarketingTipResponse response = marketingTipUseCase.getMarketingTip(tipId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.won.smarketing.recommend.presentation.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python AI 서비스 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class AIServiceRequest {
|
||||||
|
|
||||||
|
private String serviceType; // "marketing_tips", "business_insights", "trend_analysis"
|
||||||
|
private Long storeId;
|
||||||
|
private String category;
|
||||||
|
private Map<String, Object> parameters;
|
||||||
|
private Map<String, Object> context; // 매장 정보, 과거 데이터 등
|
||||||
|
}
|
||||||
@ -1,24 +1,26 @@
|
|||||||
package com.won.smarketing.recommend.presentation.dto;
|
package com.won.smarketing.recommend.presentation.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import jakarta.validation.constraints.Positive;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
import jakarta.validation.constraints.NotNull;
|
||||||
* AI 마케팅 팁 생성 요청 DTO
|
import jakarta.validation.constraints.Positive;
|
||||||
* 매장 정보를 기반으로 개인화된 마케팅 팁을 요청할 때 사용됩니다.
|
|
||||||
*/
|
@Schema(description = "마케팅 팁 생성 요청")
|
||||||
@Data
|
@Data
|
||||||
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Schema(description = "AI 마케팅 팁 생성 요청")
|
|
||||||
public class MarketingTipRequest {
|
public class MarketingTipRequest {
|
||||||
|
|
||||||
|
@Schema(description = "매장 ID", example = "1", required = true)
|
||||||
@NotNull(message = "매장 ID는 필수입니다")
|
@NotNull(message = "매장 ID는 필수입니다")
|
||||||
@Positive(message = "매장 ID는 양수여야 합니다")
|
@Positive(message = "매장 ID는 양수여야 합니다")
|
||||||
@Schema(description = "매장 ID", example = "1", required = true)
|
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "추가 요청사항", example = "여름철 음료 프로모션에 집중해주세요")
|
||||||
|
private String additionalRequirement;
|
||||||
}
|
}
|
||||||
@ -8,24 +8,61 @@ import lombok.NoArgsConstructor;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
@Schema(description = "마케팅 팁 응답")
|
||||||
* AI 마케팅 팁 생성 응답 DTO
|
|
||||||
* AI가 생성한 개인화된 마케팅 팁 정보를 전달합니다.
|
|
||||||
*/
|
|
||||||
@Data
|
@Data
|
||||||
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
|
||||||
@Schema(description = "AI 마케팅 팁 생성 응답")
|
|
||||||
public class MarketingTipResponse {
|
public class MarketingTipResponse {
|
||||||
|
|
||||||
@Schema(description = "팁 ID", example = "1")
|
@Schema(description = "팁 ID", example = "1")
|
||||||
private Long tipId;
|
private Long tipId;
|
||||||
|
|
||||||
@Schema(description = "AI 생성 마케팅 팁 내용 (100자 이내)",
|
@Schema(description = "매장 ID", example = "1")
|
||||||
example = "오늘 같은 비 오는 날에는 따뜻한 음료와 함께 실내 분위기를 강조한 포스팅을 올려보세요. #비오는날카페 #따뜻한음료 해시태그로 감성을 어필해보세요!")
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "매장명", example = "카페 봄날")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "AI 생성 마케팅 팁 내용")
|
||||||
private String tipContent;
|
private String tipContent;
|
||||||
|
|
||||||
@Schema(description = "팁 생성 시간", example = "2024-01-15T10:30:00")
|
@Schema(description = "날씨 정보")
|
||||||
|
private WeatherInfo weatherInfo;
|
||||||
|
|
||||||
|
@Schema(description = "매장 정보")
|
||||||
|
private StoreInfo storeInfo;
|
||||||
|
|
||||||
|
@Schema(description = "생성 일시")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class WeatherInfo {
|
||||||
|
@Schema(description = "온도", example = "25.5")
|
||||||
|
private Double temperature;
|
||||||
|
|
||||||
|
@Schema(description = "날씨 상태", example = "맑음")
|
||||||
|
private String condition;
|
||||||
|
|
||||||
|
@Schema(description = "습도", example = "60.0")
|
||||||
|
private Double humidity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class StoreInfo {
|
||||||
|
@Schema(description = "매장명", example = "카페 봄날")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "업종", example = "카페")
|
||||||
|
private String businessType;
|
||||||
|
|
||||||
|
@Schema(description = "위치", example = "서울시 강남구")
|
||||||
|
private String location;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,7 +7,7 @@ spring:
|
|||||||
application:
|
application:
|
||||||
name: ai-recommend-service
|
name: ai-recommend-service
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:recommenddb}
|
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:AiRecommendationDB}
|
||||||
username: ${POSTGRES_USER:postgres}
|
username: ${POSTGRES_USER:postgres}
|
||||||
password: ${POSTGRES_PASSWORD:postgres}
|
password: ${POSTGRES_PASSWORD:postgres}
|
||||||
jpa:
|
jpa:
|
||||||
@ -18,6 +18,11 @@ spring:
|
|||||||
hibernate:
|
hibernate:
|
||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
format_sql: true
|
format_sql: true
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:localhost}
|
||||||
|
port: ${REDIS_PORT:6379}
|
||||||
|
password: ${REDIS_PASSWORD:}
|
||||||
|
|
||||||
ai:
|
ai:
|
||||||
service:
|
service:
|
||||||
@ -48,3 +53,7 @@ logging:
|
|||||||
level:
|
level:
|
||||||
com.won.smarketing.recommend: ${LOG_LEVEL:DEBUG}
|
com.won.smarketing.recommend: ${LOG_LEVEL:DEBUG}
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789}
|
||||||
|
access-token-validity: ${JWT_ACCESS_VALIDITY:3600000}
|
||||||
|
refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ public class SnsContentService implements SnsContentUseCase {
|
|||||||
|
|
||||||
// 임시 콘텐츠 생성 (저장하지 않음)
|
// 임시 콘텐츠 생성 (저장하지 않음)
|
||||||
Content content = Content.builder()
|
Content content = Content.builder()
|
||||||
.contentType(ContentType.SNS_POST)
|
// .contentType(ContentType.SNS_POST)
|
||||||
.platform(platform)
|
.platform(platform)
|
||||||
.title(request.getTitle())
|
.title(request.getTitle())
|
||||||
.content(generatedContent)
|
.content(generatedContent)
|
||||||
@ -107,7 +107,7 @@ public class SnsContentService implements SnsContentUseCase {
|
|||||||
|
|
||||||
// 콘텐츠 엔티티 생성 및 저장
|
// 콘텐츠 엔티티 생성 및 저장
|
||||||
Content content = Content.builder()
|
Content content = Content.builder()
|
||||||
.contentType(ContentType.SNS_POST)
|
// .contentType(ContentType.SNS_POST)
|
||||||
.platform(Platform.fromString(request.getPlatform()))
|
.platform(Platform.fromString(request.getPlatform()))
|
||||||
.title(request.getTitle())
|
.title(request.getTitle())
|
||||||
.content(request.getContent())
|
.content(request.getContent())
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
|
||||||
package com.won.smarketing.content.application.usecase;
|
package com.won.smarketing.content.application.usecase;
|
||||||
|
|
||||||
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
|
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
|
||||||
@ -6,21 +7,19 @@ import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 포스터 콘텐츠 관련 UseCase 인터페이스
|
* 포스터 콘텐츠 관련 UseCase 인터페이스
|
||||||
* 홍보 포스터 생성 및 저장 기능 정의
|
* Clean Architecture의 Application Layer에서 비즈니스 로직 정의
|
||||||
*/
|
*/
|
||||||
public interface PosterContentUseCase {
|
public interface PosterContentUseCase {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 포스터 콘텐츠 생성
|
* 포스터 콘텐츠 생성
|
||||||
*
|
|
||||||
* @param request 포스터 콘텐츠 생성 요청
|
* @param request 포스터 콘텐츠 생성 요청
|
||||||
* @return 생성된 포스터 콘텐츠 정보
|
* @return 포스터 콘텐츠 생성 응답
|
||||||
*/
|
*/
|
||||||
PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request);
|
PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 포스터 콘텐츠 저장
|
* 포스터 콘텐츠 저장
|
||||||
*
|
|
||||||
* @param request 포스터 콘텐츠 저장 요청
|
* @param request 포스터 콘텐츠 저장 요청
|
||||||
*/
|
*/
|
||||||
void savePosterContent(PosterContentSaveRequest request);
|
void savePosterContent(PosterContentSaveRequest request);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
|
||||||
package com.won.smarketing.content.application.usecase;
|
package com.won.smarketing.content.application.usecase;
|
||||||
|
|
||||||
import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
|
import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
|
||||||
@ -6,21 +7,19 @@ import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 관련 UseCase 인터페이스
|
* SNS 콘텐츠 관련 UseCase 인터페이스
|
||||||
* SNS 게시물 생성 및 저장 기능 정의
|
* Clean Architecture의 Application Layer에서 비즈니스 로직 정의
|
||||||
*/
|
*/
|
||||||
public interface SnsContentUseCase {
|
public interface SnsContentUseCase {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 생성
|
* SNS 콘텐츠 생성
|
||||||
*
|
|
||||||
* @param request SNS 콘텐츠 생성 요청
|
* @param request SNS 콘텐츠 생성 요청
|
||||||
* @return 생성된 SNS 콘텐츠 정보
|
* @return SNS 콘텐츠 생성 응답
|
||||||
*/
|
*/
|
||||||
SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request);
|
SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 저장
|
* SNS 콘텐츠 저장
|
||||||
*
|
|
||||||
* @param request SNS 콘텐츠 저장 요청
|
* @param request SNS 콘텐츠 저장 요청
|
||||||
*/
|
*/
|
||||||
void saveSnsContent(SnsContentSaveRequest request);
|
void saveSnsContent(SnsContentSaveRequest request);
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
// marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java
|
|
||||||
package com.won.smarketing.content.config;
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JPA 설정 클래스
|
|
||||||
*
|
|
||||||
* @author smarketing-team
|
|
||||||
* @version 1.0
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EntityScan(basePackages = "com.won.smarketing.content.infrastructure.entity")
|
|
||||||
@EnableJpaRepositories(basePackages = "com.won.smarketing.content.infrastructure.repository")
|
|
||||||
public class JpaConfig {
|
|
||||||
}
|
|
||||||
@ -46,7 +46,7 @@ public class Content {
|
|||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@Column(name = "content_id")
|
@Column(name = "id")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
// ==================== 콘텐츠 분류 ====================
|
// ==================== 콘텐츠 분류 ====================
|
||||||
@ -97,8 +97,7 @@ public class Content {
|
|||||||
private ContentStatus status = ContentStatus.DRAFT;
|
private ContentStatus status = ContentStatus.DRAFT;
|
||||||
|
|
||||||
// ==================== AI 생성 조건 (Embedded) ====================
|
// ==================== AI 생성 조건 (Embedded) ====================
|
||||||
|
//@Embedded
|
||||||
@Embedded
|
|
||||||
@AttributeOverrides({
|
@AttributeOverrides({
|
||||||
@AttributeOverride(name = "toneAndManner", column = @Column(name = "tone_and_manner", length = 50)),
|
@AttributeOverride(name = "toneAndManner", column = @Column(name = "tone_and_manner", length = 50)),
|
||||||
@AttributeOverride(name = "promotionType", column = @Column(name = "promotion_type", length = 50)),
|
@AttributeOverride(name = "promotionType", column = @Column(name = "promotion_type", length = 50)),
|
||||||
@ -191,15 +190,15 @@ public class Content {
|
|||||||
* @param status 새로운 상태
|
* @param status 새로운 상태
|
||||||
* @throws IllegalStateException 잘못된 상태 전환인 경우
|
* @throws IllegalStateException 잘못된 상태 전환인 경우
|
||||||
*/
|
*/
|
||||||
public void changeStatus(ContentStatus status) {
|
// public void changeStatus(ContentStatus status) {
|
||||||
validateStatusTransition(this.status, status);
|
// validateStatusTransition(this.status, status);
|
||||||
|
//
|
||||||
if (status == ContentStatus.PUBLISHED) {
|
// if (status == ContentStatus.PUBLISHED) {
|
||||||
validateForPublication();
|
// validateForPublication();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
this.status = status;
|
// this.status = status;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 홍보 기간 설정
|
* 홍보 기간 설정
|
||||||
@ -352,9 +351,9 @@ public class Content {
|
|||||||
*
|
*
|
||||||
* @return SNS 게시물이면 true
|
* @return SNS 게시물이면 true
|
||||||
*/
|
*/
|
||||||
public boolean isSnsContent() {
|
// public boolean isSnsContent() {
|
||||||
return this.contentType == ContentType.SNS_POST;
|
// return this.contentType == ContentType.SNS_POST;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 포스터 콘텐츠 여부 확인
|
* 포스터 콘텐츠 여부 확인
|
||||||
@ -424,11 +423,11 @@ public class Content {
|
|||||||
/**
|
/**
|
||||||
* 상태 전환 유효성 검증
|
* 상태 전환 유효성 검증
|
||||||
*/
|
*/
|
||||||
private void validateStatusTransition(ContentStatus from, ContentStatus to) {
|
// private void validateStatusTransition(ContentStatus from, ContentStatus to) {
|
||||||
if (from == ContentStatus.ARCHIVED && to != ContentStatus.PUBLISHED) {
|
// if (from == ContentStatus.ARCHIVED && to != ContentStatus.PUBLISHED) {
|
||||||
throw new IllegalStateException("보관된 콘텐츠는 발행 상태로만 변경할 수 있습니다.");
|
// throw new IllegalStateException("보관된 콘텐츠는 발행 상태로만 변경할 수 있습니다.");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 발행을 위한 유효성 검증
|
* 발행을 위한 유효성 검증
|
||||||
@ -502,7 +501,7 @@ public class Content {
|
|||||||
public static Content createSnsContent(String title, String content, Platform platform,
|
public static Content createSnsContent(String title, String content, Platform platform,
|
||||||
Long storeId, CreationConditions conditions) {
|
Long storeId, CreationConditions conditions) {
|
||||||
Content snsContent = Content.builder()
|
Content snsContent = Content.builder()
|
||||||
.contentType(ContentType.SNS_POST)
|
// .contentType(ContentType.SNS_POST)
|
||||||
.platform(platform)
|
.platform(platform)
|
||||||
.title(title)
|
.title(title)
|
||||||
.content(content)
|
.content(content)
|
||||||
|
|||||||
@ -1,30 +1,53 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
|
||||||
package com.won.smarketing.content.domain.model;
|
package com.won.smarketing.content.domain.model;
|
||||||
|
|
||||||
import lombok.*;
|
import jakarta.persistence.Embeddable;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 식별자 값 객체
|
* 콘텐츠 ID 값 객체
|
||||||
* 콘텐츠의 고유 식별자를 나타내는 도메인 객체
|
* Clean Architecture의 Domain Layer에 위치하는 식별자
|
||||||
*/
|
*/
|
||||||
|
@Embeddable
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
@EqualsAndHashCode
|
|
||||||
public class ContentId {
|
public class ContentId {
|
||||||
|
|
||||||
private Long value;
|
private Long value;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ContentId 생성 팩토리 메서드
|
* ContentId 생성 팩토리 메서드
|
||||||
*
|
* @param value ID 값
|
||||||
* @param value 식별자 값
|
|
||||||
* @return ContentId 인스턴스
|
* @return ContentId 인스턴스
|
||||||
*/
|
*/
|
||||||
public static ContentId of(Long value) {
|
public static ContentId of(Long value) {
|
||||||
if (value == null || value <= 0) {
|
if (value == null || value <= 0) {
|
||||||
throw new IllegalArgumentException("ContentId는 양수여야 합니다.");
|
throw new IllegalArgumentException("ContentId 값은 양수여야 합니다.");
|
||||||
}
|
}
|
||||||
return new ContentId(value);
|
return new ContentId(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
ContentId contentId = (ContentId) o;
|
||||||
|
return Objects.equals(value, contentId.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ContentId{" + value + '}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
|
||||||
package com.won.smarketing.content.domain.model;
|
package com.won.smarketing.content.domain.model;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -5,35 +6,35 @@ import lombok.RequiredArgsConstructor;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 상태 열거형
|
* 콘텐츠 상태 열거형
|
||||||
* 콘텐츠의 생명주기 상태 정의
|
* Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum ContentStatus {
|
public enum ContentStatus {
|
||||||
|
|
||||||
DRAFT("임시저장"),
|
DRAFT("임시저장"),
|
||||||
PUBLISHED("발행됨"),
|
PUBLISHED("게시됨"),
|
||||||
ARCHIVED("보관됨");
|
SCHEDULED("예약됨"),
|
||||||
|
DELETED("삭제됨"),
|
||||||
|
PROCESSING("처리중");
|
||||||
|
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 문자열로부터 ContentStatus 변환
|
* 문자열로부터 ContentStatus 변환
|
||||||
*
|
* @param value 문자열 값
|
||||||
* @param status 상태 문자열
|
* @return ContentStatus enum
|
||||||
* @return ContentStatus
|
* @throws IllegalArgumentException 유효하지 않은 값인 경우
|
||||||
*/
|
*/
|
||||||
public static ContentStatus fromString(String status) {
|
public static ContentStatus fromString(String value) {
|
||||||
if (status == null) {
|
if (value == null) {
|
||||||
return DRAFT;
|
throw new IllegalArgumentException("ContentStatus 값은 null일 수 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ContentStatus s : ContentStatus.values()) {
|
try {
|
||||||
if (s.name().equalsIgnoreCase(status)) {
|
return ContentStatus.valueOf(value.toUpperCase());
|
||||||
return s;
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new IllegalArgumentException("유효하지 않은 ContentStatus 값입니다: " + value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("알 수 없는 콘텐츠 상태: " + status);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
|
||||||
package com.won.smarketing.content.domain.model;
|
package com.won.smarketing.content.domain.model;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -5,34 +6,34 @@ import lombok.RequiredArgsConstructor;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 타입 열거형
|
* 콘텐츠 타입 열거형
|
||||||
* 지원되는 마케팅 콘텐츠 유형 정의
|
* Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum ContentType {
|
public enum ContentType {
|
||||||
|
|
||||||
SNS_POST("SNS 게시물"),
|
SNS("SNS 게시물"),
|
||||||
POSTER("홍보 포스터");
|
POSTER("홍보 포스터"),
|
||||||
|
VIDEO("동영상"),
|
||||||
|
BLOG("블로그 포스트");
|
||||||
|
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 문자열로부터 ContentType 변환
|
* 문자열로부터 ContentType 변환
|
||||||
*
|
* @param value 문자열 값
|
||||||
* @param type 타입 문자열
|
* @return ContentType enum
|
||||||
* @return ContentType
|
* @throws IllegalArgumentException 유효하지 않은 값인 경우
|
||||||
*/
|
*/
|
||||||
public static ContentType fromString(String type) {
|
public static ContentType fromString(String value) {
|
||||||
if (type == null) {
|
if (value == null) {
|
||||||
return null;
|
throw new IllegalArgumentException("ContentType 값은 null일 수 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ContentType contentType : ContentType.values()) {
|
try {
|
||||||
if (contentType.name().equalsIgnoreCase(type)) {
|
return ContentType.valueOf(value.toUpperCase());
|
||||||
return contentType;
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new IllegalArgumentException("유효하지 않은 ContentType 값입니다: " + value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("알 수 없는 콘텐츠 타입: " + type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,66 +1,68 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
|
||||||
package com.won.smarketing.content.domain.model;
|
package com.won.smarketing.content.domain.model;
|
||||||
|
|
||||||
import lombok.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 생성 조건 도메인 모델
|
* 콘텐츠 생성 조건 도메인 모델
|
||||||
* AI 콘텐츠 생성 시 사용되는 조건 정보
|
* Clean Architecture의 Domain Layer에 위치하는 값 객체
|
||||||
*/
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "contents_conditions")
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder(toBuilder = true)
|
@Builder
|
||||||
public class CreationConditions {
|
public class CreationConditions {
|
||||||
|
|
||||||
/**
|
@Id
|
||||||
* 홍보 대상 카테고리
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
*/
|
private Long id;
|
||||||
|
|
||||||
|
//@OneToOne(mappedBy = "creationConditions")
|
||||||
|
@Column(name = "content", length = 100)
|
||||||
|
private Content content;
|
||||||
|
|
||||||
|
@Column(name = "category", length = 100)
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
/**
|
@Column(name = "requirement", columnDefinition = "TEXT")
|
||||||
* 특별 요구사항
|
|
||||||
*/
|
|
||||||
private String requirement;
|
private String requirement;
|
||||||
|
|
||||||
/**
|
@Column(name = "tone_and_manner", length = 100)
|
||||||
* 톤앤매너
|
|
||||||
*/
|
|
||||||
private String toneAndManner;
|
private String toneAndManner;
|
||||||
|
|
||||||
/**
|
@Column(name = "emotion_intensity", length = 100)
|
||||||
* 감정 강도
|
|
||||||
*/
|
|
||||||
private String emotionIntensity;
|
private String emotionIntensity;
|
||||||
|
|
||||||
/**
|
@Column(name = "event_name", length = 200)
|
||||||
* 이벤트명
|
|
||||||
*/
|
|
||||||
private String eventName;
|
private String eventName;
|
||||||
|
|
||||||
/**
|
@Column(name = "start_date")
|
||||||
* 홍보 시작일
|
|
||||||
*/
|
|
||||||
private LocalDate startDate;
|
private LocalDate startDate;
|
||||||
|
|
||||||
/**
|
@Column(name = "end_date")
|
||||||
* 홍보 종료일
|
|
||||||
*/
|
|
||||||
private LocalDate endDate;
|
private LocalDate endDate;
|
||||||
|
|
||||||
/**
|
@Column(name = "photo_style", length = 100)
|
||||||
* 사진 스타일 (포스터용)
|
|
||||||
*/
|
|
||||||
private String photoStyle;
|
private String photoStyle;
|
||||||
|
|
||||||
/**
|
@Column(name = "promotionType", length = 100)
|
||||||
* 타겟 고객
|
|
||||||
*/
|
|
||||||
private String targetAudience;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 프로모션 타입
|
|
||||||
*/
|
|
||||||
private String promotionType;
|
private String promotionType;
|
||||||
|
|
||||||
|
public CreationConditions(String category, String requirement, String toneAndManner, String emotionIntensity, String eventName, LocalDate startDate, LocalDate endDate, String photoStyle, String promotionType) {
|
||||||
|
}
|
||||||
|
// /**
|
||||||
|
// * 콘텐츠와의 연관관계 설정
|
||||||
|
// * @param content 연관된 콘텐츠
|
||||||
|
// */
|
||||||
|
// public void setContent(Content content) {
|
||||||
|
// this.content = content;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
|
||||||
package com.won.smarketing.content.domain.model;
|
package com.won.smarketing.content.domain.model;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -5,7 +6,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 플랫폼 열거형
|
* 플랫폼 열거형
|
||||||
* 콘텐츠가 게시될 플랫폼 정의
|
* Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -13,27 +14,28 @@ public enum Platform {
|
|||||||
|
|
||||||
INSTAGRAM("인스타그램"),
|
INSTAGRAM("인스타그램"),
|
||||||
NAVER_BLOG("네이버 블로그"),
|
NAVER_BLOG("네이버 블로그"),
|
||||||
GENERAL("범용");
|
FACEBOOK("페이스북"),
|
||||||
|
KAKAO_STORY("카카오스토리"),
|
||||||
|
YOUTUBE("유튜브"),
|
||||||
|
GENERAL("일반");
|
||||||
|
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 문자열로부터 Platform 변환
|
* 문자열로부터 Platform 변환
|
||||||
*
|
* @param value 문자열 값
|
||||||
* @param platform 플랫폼 문자열
|
* @return Platform enum
|
||||||
* @return Platform
|
* @throws IllegalArgumentException 유효하지 않은 값인 경우
|
||||||
*/
|
*/
|
||||||
public static Platform fromString(String platform) {
|
public static Platform fromString(String value) {
|
||||||
if (platform == null) {
|
if (value == null) {
|
||||||
return GENERAL;
|
throw new IllegalArgumentException("Platform 값은 null일 수 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Platform p : Platform.values()) {
|
try {
|
||||||
if (p.name().equalsIgnoreCase(platform)) {
|
return Platform.valueOf(value.toUpperCase());
|
||||||
return p;
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new IllegalArgumentException("유효하지 않은 Platform 값입니다: " + value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("알 수 없는 플랫폼: " + platform);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,40 +1,36 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
|
||||||
package com.won.smarketing.content.domain.repository;
|
package com.won.smarketing.content.domain.repository;
|
||||||
|
|
||||||
import com.won.smarketing.content.domain.model.Content;
|
import com.won.smarketing.content.domain.model.Content;
|
||||||
import com.won.smarketing.content.domain.model.ContentId;
|
import com.won.smarketing.content.domain.model.ContentId;
|
||||||
import com.won.smarketing.content.domain.model.ContentType;
|
import com.won.smarketing.content.domain.model.ContentType;
|
||||||
import com.won.smarketing.content.domain.model.Platform;
|
import com.won.smarketing.content.domain.model.Platform;
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 저장소 인터페이스
|
* 콘텐츠 리포지토리 인터페이스
|
||||||
* 콘텐츠 도메인의 데이터 접근 추상화
|
* Clean Architecture의 Domain Layer에서 데이터 접근 정의
|
||||||
*/
|
*/
|
||||||
@Repository
|
|
||||||
public interface ContentRepository {
|
public interface ContentRepository {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 저장
|
* 콘텐츠 저장
|
||||||
*
|
|
||||||
* @param content 저장할 콘텐츠
|
* @param content 저장할 콘텐츠
|
||||||
* @return 저장된 콘텐츠
|
* @return 저장된 콘텐츠
|
||||||
*/
|
*/
|
||||||
Content save(Content content);
|
Content save(Content content);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 ID로 조회
|
* ID로 콘텐츠 조회
|
||||||
*
|
|
||||||
* @param id 콘텐츠 ID
|
* @param id 콘텐츠 ID
|
||||||
* @return 콘텐츠 (Optional)
|
* @return 조회된 콘텐츠
|
||||||
*/
|
*/
|
||||||
Optional<Content> findById(ContentId id);
|
Optional<Content> findById(ContentId id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 필터 조건으로 콘텐츠 목록 조회
|
* 필터 조건으로 콘텐츠 목록 조회
|
||||||
*
|
|
||||||
* @param contentType 콘텐츠 타입
|
* @param contentType 콘텐츠 타입
|
||||||
* @param platform 플랫폼
|
* @param platform 플랫폼
|
||||||
* @param period 기간
|
* @param period 기간
|
||||||
@ -45,15 +41,13 @@ public interface ContentRepository {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행 중인 콘텐츠 목록 조회
|
* 진행 중인 콘텐츠 목록 조회
|
||||||
*
|
|
||||||
* @param period 기간
|
* @param period 기간
|
||||||
* @return 진행 중인 콘텐츠 목록
|
* @return 진행 중인 콘텐츠 목록
|
||||||
*/
|
*/
|
||||||
List<Content> findOngoingContents(String period);
|
List<Content> findOngoingContents(String period);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 삭제
|
* ID로 콘텐츠 삭제
|
||||||
*
|
|
||||||
* @param id 삭제할 콘텐츠 ID
|
* @param id 삭제할 콘텐츠 ID
|
||||||
*/
|
*/
|
||||||
void deleteById(ContentId id);
|
void deleteById(ContentId id);
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.won.smarketing.content.domain.repository;
|
||||||
|
import com.won.smarketing.content.infrastructure.entity.ContentEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA ContentRepository
|
||||||
|
* JPA 기반 콘텐츠 데이터 접근
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface SpringDataContentRepository extends JpaRepository<ContentEntity, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장별 콘텐츠 조회
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @return 콘텐츠 목록
|
||||||
|
*/
|
||||||
|
List<ContentEntity> findByStoreId(Long storeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 타입별 조회
|
||||||
|
*
|
||||||
|
* @param contentType 콘텐츠 타입
|
||||||
|
* @return 콘텐츠 목록
|
||||||
|
*/
|
||||||
|
List<ContentEntity> findByContentType(String contentType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플랫폼별 조회
|
||||||
|
*
|
||||||
|
* @param platform 플랫폼
|
||||||
|
* @return 콘텐츠 목록
|
||||||
|
*/
|
||||||
|
List<ContentEntity> findByPlatform(String platform);
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
|
||||||
package com.won.smarketing.content.infrastructure.entity;
|
package com.won.smarketing.content.infrastructure.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@ -8,24 +10,20 @@ import lombok.Setter;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 조건 JPA 엔티티
|
* 콘텐츠 생성 조건 JPA 엔티티
|
||||||
*
|
|
||||||
* @author smarketing-team
|
|
||||||
* @version 1.0
|
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "contents_conditions")
|
@Table(name = "content_conditions")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
|
||||||
public class ContentConditionsJpaEntity {
|
public class ContentConditionsJpaEntity {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "content_id")
|
@JoinColumn(name = "content_id", nullable = false)
|
||||||
private ContentJpaEntity content;
|
private ContentJpaEntity content;
|
||||||
|
|
||||||
@Column(name = "category", length = 100)
|
@Column(name = "category", length = 100)
|
||||||
@ -37,7 +35,7 @@ public class ContentConditionsJpaEntity {
|
|||||||
@Column(name = "tone_and_manner", length = 100)
|
@Column(name = "tone_and_manner", length = 100)
|
||||||
private String toneAndManner;
|
private String toneAndManner;
|
||||||
|
|
||||||
@Column(name = "emotion_intensity", length = 100)
|
@Column(name = "emotion_intensity", length = 50)
|
||||||
private String emotionIntensity;
|
private String emotionIntensity;
|
||||||
|
|
||||||
@Column(name = "event_name", length = 200)
|
@Column(name = "event_name", length = 200)
|
||||||
@ -52,9 +50,9 @@ public class ContentConditionsJpaEntity {
|
|||||||
@Column(name = "photo_style", length = 100)
|
@Column(name = "photo_style", length = 100)
|
||||||
private String photoStyle;
|
private String photoStyle;
|
||||||
|
|
||||||
@Column(name = "TargetAudience", length = 100)
|
@Column(name = "target_audience", length = 200)
|
||||||
private String targetAudience;
|
private String targetAudience;
|
||||||
|
|
||||||
@Column(name = "PromotionType", length = 100)
|
@Column(name = "promotion_type", length = 100)
|
||||||
private String PromotionType;
|
private String promotionType;
|
||||||
}
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.won.smarketing.content.infrastructure.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 엔티티
|
||||||
|
* 콘텐츠 정보를 데이터베이스에 저장하기 위한 JPA 엔티티
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "contents")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public class ContentEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "content_type", nullable = false)
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
@Column(name = "platform", nullable = false)
|
||||||
|
private String platform;
|
||||||
|
|
||||||
|
@Column(name = "title", nullable = false)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(name = "content", columnDefinition = "TEXT")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Column(name = "hashtags")
|
||||||
|
private String hashtags;
|
||||||
|
|
||||||
|
@Column(name = "images", columnDefinition = "TEXT")
|
||||||
|
private String images;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "store_id", nullable = false)
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -1,27 +1,24 @@
|
|||||||
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
|
|
||||||
package com.won.smarketing.content.infrastructure.entity;
|
package com.won.smarketing.content.infrastructure.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
import org.hibernate.annotations.UpdateTimestamp;
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 JPA 엔티티
|
* 콘텐츠 JPA 엔티티
|
||||||
*
|
|
||||||
* @author smarketing-team
|
|
||||||
* @version 1.0
|
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "contents")
|
@Table(name = "contents")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
public class ContentJpaEntity {
|
public class ContentJpaEntity {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -43,24 +40,24 @@ public class ContentJpaEntity {
|
|||||||
@Column(name = "content", columnDefinition = "TEXT")
|
@Column(name = "content", columnDefinition = "TEXT")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@Column(name = "hashtags", columnDefinition = "JSON")
|
@Column(name = "hashtags", columnDefinition = "TEXT")
|
||||||
private String hashtags;
|
private String hashtags;
|
||||||
|
|
||||||
@Column(name = "images", columnDefinition = "JSON")
|
@Column(name = "images", columnDefinition = "TEXT")
|
||||||
private String images;
|
private String images;
|
||||||
|
|
||||||
@Column(name = "status", length = 50)
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@CreationTimestamp
|
@CreatedDate
|
||||||
@Column(name = "created_at")
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
@UpdateTimestamp
|
@LastModifiedDate
|
||||||
@Column(name = "updated_at")
|
@Column(name = "updated_at")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
// 연관 엔티티
|
// CreationConditions와의 관계 - OneToOne으로 별도 엔티티로 관리
|
||||||
@OneToOne(mappedBy = "content", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
@OneToOne(mappedBy = "content", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
private ContentConditionsJpaEntity conditions;
|
private ContentConditionsJpaEntity conditions;
|
||||||
}
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java
|
||||||
|
package com.won.smarketing.content.infrastructure.external;
|
||||||
|
|
||||||
|
import com.won.smarketing.content.domain.model.Platform;
|
||||||
|
import com.won.smarketing.content.domain.model.CreationConditions;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 콘텐츠 생성 인터페이스
|
||||||
|
* Clean Architecture의 Infrastructure Layer에서 외부 AI 서비스와의 연동 정의
|
||||||
|
*/
|
||||||
|
public interface AiContentGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SNS 콘텐츠 생성
|
||||||
|
* @param title 제목
|
||||||
|
* @param category 카테고리
|
||||||
|
* @param platform 플랫폼
|
||||||
|
* @param conditions 생성 조건
|
||||||
|
* @return 생성된 콘텐츠 텍스트
|
||||||
|
*/
|
||||||
|
String generateSnsContent(String title, String category, Platform platform, CreationConditions conditions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 해시태그 생성
|
||||||
|
* @param content 콘텐츠 내용
|
||||||
|
* @param platform 플랫폼
|
||||||
|
* @return 생성된 해시태그 목록
|
||||||
|
*/
|
||||||
|
List<String> generateHashtags(String content, Platform platform);
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java
|
||||||
|
package com.won.smarketing.content.infrastructure.external;
|
||||||
|
|
||||||
|
import com.won.smarketing.content.domain.model.CreationConditions;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 포스터 생성 인터페이스
|
||||||
|
* Clean Architecture의 Infrastructure Layer에서 외부 AI 서비스와의 연동 정의
|
||||||
|
*/
|
||||||
|
public interface AiPosterGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 이미지 생성
|
||||||
|
* @param title 제목
|
||||||
|
* @param category 카테고리
|
||||||
|
* @param conditions 생성 조건
|
||||||
|
* @return 생성된 포스터 이미지 URL
|
||||||
|
*/
|
||||||
|
String generatePoster(String title, String category, CreationConditions conditions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 다양한 사이즈 생성
|
||||||
|
* @param originalImage 원본 이미지 URL
|
||||||
|
* @return 사이즈별 이미지 URL 맵
|
||||||
|
*/
|
||||||
|
Map<String, String> generatePosterSizes(String originalImage);
|
||||||
|
}
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java
|
||||||
|
package com.won.smarketing.content.infrastructure.external;
|
||||||
|
|
||||||
|
import com.won.smarketing.content.domain.model.Platform;
|
||||||
|
import com.won.smarketing.content.domain.model.CreationConditions;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude AI를 활용한 콘텐츠 생성 구현체
|
||||||
|
* Clean Architecture의 Infrastructure Layer에 위치
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class ClaudeAiContentGenerator implements AiContentGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SNS 콘텐츠 생성
|
||||||
|
* Claude AI API를 호출하여 SNS 게시물을 생성합니다.
|
||||||
|
*
|
||||||
|
* @param title 제목
|
||||||
|
* @param category 카테고리
|
||||||
|
* @param platform 플랫폼
|
||||||
|
* @param conditions 생성 조건
|
||||||
|
* @return 생성된 콘텐츠 텍스트
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String generateSnsContent(String title, String category, Platform platform, CreationConditions conditions) {
|
||||||
|
try {
|
||||||
|
// Claude AI API 호출 로직 (실제 구현에서는 HTTP 클라이언트를 사용)
|
||||||
|
String prompt = buildContentPrompt(title, category, platform, conditions);
|
||||||
|
|
||||||
|
// TODO: 실제 Claude AI API 호출
|
||||||
|
// 현재는 더미 데이터 반환
|
||||||
|
return generateDummySnsContent(title, platform);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AI 콘텐츠 생성 실패: {}", e.getMessage(), e);
|
||||||
|
return generateFallbackContent(title, platform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 해시태그 생성
|
||||||
|
* 콘텐츠 내용을 분석하여 관련 해시태그를 생성합니다.
|
||||||
|
*
|
||||||
|
* @param content 콘텐츠 내용
|
||||||
|
* @param platform 플랫폼
|
||||||
|
* @return 생성된 해시태그 목록
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<String> generateHashtags(String content, Platform platform) {
|
||||||
|
try {
|
||||||
|
// TODO: 실제 Claude AI API 호출하여 해시태그 생성
|
||||||
|
// 현재는 더미 데이터 반환
|
||||||
|
return generateDummyHashtags(platform);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("해시태그 생성 실패: {}", e.getMessage(), e);
|
||||||
|
return Arrays.asList("#맛집", "#신메뉴", "#추천");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 프롬프트 생성
|
||||||
|
*/
|
||||||
|
private String buildContentPrompt(String title, String category, Platform platform, CreationConditions conditions) {
|
||||||
|
StringBuilder prompt = new StringBuilder();
|
||||||
|
prompt.append("다음 조건에 맞는 ").append(platform.getDisplayName()).append(" 게시물을 작성해주세요:\n");
|
||||||
|
prompt.append("제목: ").append(title).append("\n");
|
||||||
|
prompt.append("카테고리: ").append(category).append("\n");
|
||||||
|
|
||||||
|
if (conditions.getRequirement() != null) {
|
||||||
|
prompt.append("요구사항: ").append(conditions.getRequirement()).append("\n");
|
||||||
|
}
|
||||||
|
if (conditions.getToneAndManner() != null) {
|
||||||
|
prompt.append("톤앤매너: ").append(conditions.getToneAndManner()).append("\n");
|
||||||
|
}
|
||||||
|
if (conditions.getEmotionIntensity() != null) {
|
||||||
|
prompt.append("감정 강도: ").append(conditions.getEmotionIntensity()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 더미 SNS 콘텐츠 생성 (개발용)
|
||||||
|
*/
|
||||||
|
private String generateDummySnsContent(String title, Platform platform) {
|
||||||
|
switch (platform) {
|
||||||
|
case INSTAGRAM:
|
||||||
|
return String.format("🎉 %s\n\n맛있는 순간을 놓치지 마세요! 새로운 맛의 경험이 여러분을 기다리고 있어요. 따뜻한 분위기에서 즐기는 특별한 시간을 만들어보세요.\n\n📍 지금 바로 방문해보세요!", title);
|
||||||
|
case NAVER_BLOG:
|
||||||
|
return String.format("안녕하세요! 오늘은 %s에 대해 소개해드리려고 해요.\n\n정성스럽게 준비한 새로운 메뉴로 고객 여러분께 더 나은 경험을 선사하고 싶습니다. 많은 관심과 사랑 부탁드려요!", title);
|
||||||
|
default:
|
||||||
|
return String.format("%s - 새로운 경험을 만나보세요!", title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 더미 해시태그 생성 (개발용)
|
||||||
|
*/
|
||||||
|
private List<String> generateDummyHashtags(Platform platform) {
|
||||||
|
switch (platform) {
|
||||||
|
case INSTAGRAM:
|
||||||
|
return Arrays.asList("#맛집", "#신메뉴", "#인스타그램", "#데일리", "#추천", "#음식스타그램");
|
||||||
|
case NAVER_BLOG:
|
||||||
|
return Arrays.asList("#맛집", "#리뷰", "#추천", "#신메뉴", "#블로그");
|
||||||
|
default:
|
||||||
|
return Arrays.asList("#맛집", "#신메뉴", "#추천");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 폴백 콘텐츠 생성 (AI 서비스 실패 시)
|
||||||
|
*/
|
||||||
|
private String generateFallbackContent(String title, Platform platform) {
|
||||||
|
return String.format("🎉 %s\n\n새로운 소식을 전해드립니다. 많은 관심 부탁드려요!", title);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java
|
||||||
|
package com.won.smarketing.content.infrastructure.external;
|
||||||
|
|
||||||
|
import com.won.smarketing.content.domain.model.CreationConditions;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude AI를 활용한 포스터 생성 구현체
|
||||||
|
* Clean Architecture의 Infrastructure Layer에 위치
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class ClaudeAiPosterGenerator implements AiPosterGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 이미지 생성
|
||||||
|
* Claude AI API를 호출하여 홍보 포스터를 생성합니다.
|
||||||
|
*
|
||||||
|
* @param title 제목
|
||||||
|
* @param category 카테고리
|
||||||
|
* @param conditions 생성 조건
|
||||||
|
* @return 생성된 포스터 이미지 URL
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String generatePoster(String title, String category, CreationConditions conditions) {
|
||||||
|
try {
|
||||||
|
// Claude AI API 호출 로직 (실제 구현에서는 HTTP 클라이언트를 사용)
|
||||||
|
String prompt = buildPosterPrompt(title, category, conditions);
|
||||||
|
|
||||||
|
// TODO: 실제 Claude AI API 호출
|
||||||
|
// 현재는 더미 데이터 반환
|
||||||
|
return generateDummyPosterUrl(title);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AI 포스터 생성 실패: {}", e.getMessage(), e);
|
||||||
|
return generateFallbackPosterUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 다양한 사이즈 생성
|
||||||
|
* 원본 포스터를 기반으로 다양한 사이즈의 포스터를 생성합니다.
|
||||||
|
*
|
||||||
|
* @param originalImage 원본 이미지 URL
|
||||||
|
* @return 사이즈별 이미지 URL 맵
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, String> generatePosterSizes(String originalImage) {
|
||||||
|
try {
|
||||||
|
// TODO: 실제 이미지 리사이징 API 호출
|
||||||
|
// 현재는 더미 데이터 반환
|
||||||
|
return generateDummyPosterSizes(originalImage);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("포스터 사이즈 생성 실패: {}", e.getMessage(), e);
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 포스터 프롬프트 생성
|
||||||
|
*/
|
||||||
|
private String buildPosterPrompt(String title, String category, CreationConditions conditions) {
|
||||||
|
StringBuilder prompt = new StringBuilder();
|
||||||
|
prompt.append("다음 조건에 맞는 홍보 포스터를 생성해주세요:\n");
|
||||||
|
prompt.append("제목: ").append(title).append("\n");
|
||||||
|
prompt.append("카테고리: ").append(category).append("\n");
|
||||||
|
|
||||||
|
if (conditions.getPhotoStyle() != null) {
|
||||||
|
prompt.append("사진 스타일: ").append(conditions.getPhotoStyle()).append("\n");
|
||||||
|
}
|
||||||
|
if (conditions.getRequirement() != null) {
|
||||||
|
prompt.append("요구사항: ").append(conditions.getRequirement()).append("\n");
|
||||||
|
}
|
||||||
|
if (conditions.getToneAndManner() != null) {
|
||||||
|
prompt.append("톤앤매너: ").append(conditions.getToneAndManner()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 더미 포스터 URL 생성 (개발용)
|
||||||
|
*/
|
||||||
|
private String generateDummyPosterUrl(String title) {
|
||||||
|
return String.format("https://example.com/posters/%s-poster.jpg",
|
||||||
|
title.replaceAll("\\s+", "-").toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 더미 포스터 사이즈별 URL 생성 (개발용)
|
||||||
|
*/
|
||||||
|
private Map<String, String> generateDummyPosterSizes(String originalImage) {
|
||||||
|
Map<String, String> sizes = new HashMap<>();
|
||||||
|
String baseUrl = originalImage.substring(0, originalImage.lastIndexOf("."));
|
||||||
|
String extension = originalImage.substring(originalImage.lastIndexOf("."));
|
||||||
|
|
||||||
|
sizes.put("small", baseUrl + "-small" + extension);
|
||||||
|
sizes.put("medium", baseUrl + "-medium" + extension);
|
||||||
|
sizes.put("large", baseUrl + "-large" + extension);
|
||||||
|
sizes.put("xlarge", baseUrl + "-xlarge" + extension);
|
||||||
|
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 폴백 포스터 URL 생성 (AI 서비스 실패 시)
|
||||||
|
*/
|
||||||
|
private String generateFallbackPosterUrl() {
|
||||||
|
return "https://example.com/posters/default-poster.jpg";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -91,7 +91,7 @@ public class ContentMapper {
|
|||||||
entity.getConditions().getStartDate(),
|
entity.getConditions().getStartDate(),
|
||||||
entity.getConditions().getEndDate(),
|
entity.getConditions().getEndDate(),
|
||||||
entity.getConditions().getPhotoStyle(),
|
entity.getConditions().getPhotoStyle(),
|
||||||
entity.getConditions().getTargetAudience(),
|
// entity.getConditions().getTargetAudience(),
|
||||||
entity.getConditions().getPromotionType()
|
entity.getConditions().getPromotionType()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
|
||||||
package com.won.smarketing.content.infrastructure.repository;
|
package com.won.smarketing.content.infrastructure.repository;
|
||||||
|
|
||||||
import com.won.smarketing.content.domain.model.Content;
|
import com.won.smarketing.content.domain.model.Content;
|
||||||
@ -5,60 +6,44 @@ import com.won.smarketing.content.domain.model.ContentId;
|
|||||||
import com.won.smarketing.content.domain.model.ContentType;
|
import com.won.smarketing.content.domain.model.ContentType;
|
||||||
import com.won.smarketing.content.domain.model.Platform;
|
import com.won.smarketing.content.domain.model.Platform;
|
||||||
import com.won.smarketing.content.domain.repository.ContentRepository;
|
import com.won.smarketing.content.domain.repository.ContentRepository;
|
||||||
import com.won.smarketing.content.infrastructure.entity.ContentJpaEntity;
|
|
||||||
import com.won.smarketing.content.infrastructure.mapper.ContentMapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JPA 기반 콘텐츠 Repository 구현체
|
* JPA를 활용한 콘텐츠 리포지토리 구현체
|
||||||
*
|
* Clean Architecture의 Infrastructure Layer에 위치
|
||||||
* @author smarketing-team
|
|
||||||
* @version 1.0
|
|
||||||
*/
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
|
||||||
public class JpaContentRepository implements ContentRepository {
|
public class JpaContentRepository implements ContentRepository {
|
||||||
|
|
||||||
private final SpringDataContentRepository springDataContentRepository;
|
private final JpaContentRepositoryInterface jpaRepository;
|
||||||
private final ContentMapper contentMapper;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠를 저장합니다.
|
* 콘텐츠 저장
|
||||||
*
|
|
||||||
* @param content 저장할 콘텐츠
|
* @param content 저장할 콘텐츠
|
||||||
* @return 저장된 콘텐츠
|
* @return 저장된 콘텐츠
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Content save(Content content) {
|
public Content save(Content content) {
|
||||||
log.debug("Saving content: {}", content.getId());
|
return jpaRepository.save(content);
|
||||||
ContentJpaEntity entity = contentMapper.toEntity(content);
|
|
||||||
ContentJpaEntity savedEntity = springDataContentRepository.save(entity);
|
|
||||||
return contentMapper.toDomain(savedEntity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID로 콘텐츠를 조회합니다.
|
* ID로 콘텐츠 조회
|
||||||
*
|
|
||||||
* @param id 콘텐츠 ID
|
* @param id 콘텐츠 ID
|
||||||
* @return 조회된 콘텐츠
|
* @return 조회된 콘텐츠
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Optional<Content> findById(ContentId id) {
|
public Optional<Content> findById(ContentId id) {
|
||||||
log.debug("Finding content by id: {}", id.getValue());
|
return jpaRepository.findById(id.getValue());
|
||||||
return springDataContentRepository.findById(id.getValue())
|
|
||||||
.map(contentMapper::toDomain);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 필터 조건으로 콘텐츠 목록을 조회합니다.
|
* 필터 조건으로 콘텐츠 목록 조회
|
||||||
*
|
|
||||||
* @param contentType 콘텐츠 타입
|
* @param contentType 콘텐츠 타입
|
||||||
* @param platform 플랫폼
|
* @param platform 플랫폼
|
||||||
* @param period 기간
|
* @param period 기간
|
||||||
@ -67,45 +52,25 @@ public class JpaContentRepository implements ContentRepository {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<Content> findByFilters(ContentType contentType, Platform platform, String period, String sortBy) {
|
public List<Content> findByFilters(ContentType contentType, Platform platform, String period, String sortBy) {
|
||||||
log.debug("Finding contents by filters - type: {}, platform: {}, period: {}, sortBy: {}",
|
return jpaRepository.findByFilters(contentType, platform, period, sortBy);
|
||||||
contentType, platform, period, sortBy);
|
|
||||||
|
|
||||||
List<ContentJpaEntity> entities = springDataContentRepository.findByFilters(
|
|
||||||
contentType != null ? contentType.name() : null,
|
|
||||||
platform != null ? platform.name() : null,
|
|
||||||
period,
|
|
||||||
sortBy
|
|
||||||
);
|
|
||||||
|
|
||||||
return entities.stream()
|
|
||||||
.map(contentMapper::toDomain)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행 중인 콘텐츠 목록을 조회합니다.
|
* 진행 중인 콘텐츠 목록 조회
|
||||||
*
|
|
||||||
* @param period 기간
|
* @param period 기간
|
||||||
* @return 진행 중인 콘텐츠 목록
|
* @return 진행 중인 콘텐츠 목록
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<Content> findOngoingContents(String period) {
|
public List<Content> findOngoingContents(String period) {
|
||||||
log.debug("Finding ongoing contents for period: {}", period);
|
return jpaRepository.findOngoingContents(period);
|
||||||
List<ContentJpaEntity> entities = springDataContentRepository.findOngoingContents(period);
|
|
||||||
|
|
||||||
return entities.stream()
|
|
||||||
.map(contentMapper::toDomain)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID로 콘텐츠를 삭제합니다.
|
* ID로 콘텐츠 삭제
|
||||||
*
|
* @param id 삭제할 콘텐츠 ID
|
||||||
* @param id 콘텐츠 ID
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void deleteById(ContentId id) {
|
public void deleteById(ContentId id) {
|
||||||
log.debug("Deleting content by id: {}", id.getValue());
|
jpaRepository.deleteById(id.getValue());
|
||||||
springDataContentRepository.deleteById(id.getValue());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java
|
||||||
|
package com.won.smarketing.content.infrastructure.repository;
|
||||||
|
|
||||||
|
import com.won.smarketing.content.domain.model.Content;
|
||||||
|
import com.won.smarketing.content.domain.model.ContentType;
|
||||||
|
import com.won.smarketing.content.domain.model.Platform;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA 콘텐츠 리포지토리 인터페이스
|
||||||
|
* Clean Architecture의 Infrastructure Layer에 위치
|
||||||
|
*/
|
||||||
|
public interface JpaContentRepositoryInterface extends JpaRepository<Content, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 필터 조건으로 콘텐츠 목록 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT c FROM Content c WHERE " +
|
||||||
|
"(:contentType IS NULL OR c.contentType = :contentType) AND " +
|
||||||
|
"(:platform IS NULL OR c.platform = :platform) AND " +
|
||||||
|
"(:period IS NULL OR " +
|
||||||
|
" (:period = 'week' AND c.createdAt >= CURRENT_DATE - 7) OR " +
|
||||||
|
" (:period = 'month' AND c.createdAt >= CURRENT_DATE - 30) OR " +
|
||||||
|
" (:period = 'year' AND c.createdAt >= CURRENT_DATE - 365)) " +
|
||||||
|
"ORDER BY " +
|
||||||
|
"CASE WHEN :sortBy = 'latest' THEN c.createdAt END DESC, " +
|
||||||
|
"CASE WHEN :sortBy = 'oldest' THEN c.createdAt END ASC, " +
|
||||||
|
"CASE WHEN :sortBy = 'title' THEN c.title END ASC")
|
||||||
|
List<Content> findByFilters(@Param("contentType") ContentType contentType,
|
||||||
|
@Param("platform") Platform platform,
|
||||||
|
@Param("period") String period,
|
||||||
|
@Param("sortBy") String sortBy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 진행 중인 콘텐츠 목록 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT c FROM Content c WHERE " +
|
||||||
|
"c.status IN ('PUBLISHED', 'SCHEDULED') AND " +
|
||||||
|
"(:period IS NULL OR " +
|
||||||
|
" (:period = 'week' AND c.createdAt >= CURRENT_DATE - 7) OR " +
|
||||||
|
" (:period = 'month' AND c.createdAt >= CURRENT_DATE - 30) OR " +
|
||||||
|
" (:period = 'year' AND c.createdAt >= CURRENT_DATE - 365)) " +
|
||||||
|
"ORDER BY c.createdAt DESC")
|
||||||
|
List<Content> findOngoingContents(@Param("period") String period);
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java
|
||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 생성 조건 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "콘텐츠 생성 조건")
|
||||||
|
public class CreationConditionsDto {
|
||||||
|
|
||||||
|
@Schema(description = "카테고리", example = "음식")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "생성 요구사항", example = "젊은 고객층을 타겟으로 한 재미있는 콘텐츠")
|
||||||
|
private String requirement;
|
||||||
|
|
||||||
|
@Schema(description = "톤앤매너", example = "친근하고 활발한")
|
||||||
|
private String toneAndManner;
|
||||||
|
|
||||||
|
@Schema(description = "감정 강도", example = "보통")
|
||||||
|
private String emotionIntensity;
|
||||||
|
|
||||||
|
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
|
||||||
|
private String eventName;
|
||||||
|
|
||||||
|
@Schema(description = "시작일")
|
||||||
|
private LocalDate startDate;
|
||||||
|
|
||||||
|
@Schema(description = "종료일")
|
||||||
|
private LocalDate endDate;
|
||||||
|
|
||||||
|
@Schema(description = "사진 스타일", example = "모던하고 깔끔한")
|
||||||
|
private String photoStyle;
|
||||||
|
}
|
||||||
@ -306,7 +306,7 @@ public class SnsContentCreateResponse {
|
|||||||
// 생성 조건 정보 설정
|
// 생성 조건 정보 설정
|
||||||
if (content.getCreationConditions() != null) {
|
if (content.getCreationConditions() != null) {
|
||||||
builder.generationConditions(GenerationConditionsDto.builder()
|
builder.generationConditions(GenerationConditionsDto.builder()
|
||||||
.targetAudience(content.getCreationConditions().getTargetAudience())
|
//.targetAudience(content.getCreationConditions().getTargetAudience())
|
||||||
.eventName(content.getCreationConditions().getEventName())
|
.eventName(content.getCreationConditions().getEventName())
|
||||||
.toneAndManner(content.getCreationConditions().getToneAndManner())
|
.toneAndManner(content.getCreationConditions().getToneAndManner())
|
||||||
.promotionType(content.getCreationConditions().getPromotionType())
|
.promotionType(content.getCreationConditions().getPromotionType())
|
||||||
|
|||||||
@ -11,8 +11,8 @@ spring:
|
|||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
ddl-auto: ${DDL_AUTO:update}
|
||||||
show-sql: ${JPA_SHOW_SQL:true}
|
show-sql: ${SHOW_SQL:true}
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user