Merge branch 'main' of https://github.com/won-ktds/smarketing-backend into poster-content

This commit is contained in:
yuhalog 2025-06-18 17:00:08 +09:00
commit 9004f7b726
10 changed files with 78 additions and 202 deletions

View File

@ -35,24 +35,24 @@ podTemplate(
echo "Team ID: ${props.teamid}" echo "Team ID: ${props.teamid}"
} }
stage("Check Changes") { // stage("Check Changes") {
script { // script {
def changes = sh( // def changes = sh(
script: "git diff --name-only HEAD~1 HEAD", // script: "git diff --name-only HEAD~1 HEAD",
returnStdout: true // returnStdout: true
).trim() // ).trim()
//
echo "Changed files: ${changes}" // echo "Changed files: ${changes}"
//
if (!changes.contains("smarketing-ai/")) { // if (!changes.contains("smarketing-ai/")) {
echo "No changes in smarketing-ai, skipping build" // echo "No changes in smarketing-ai, skipping build"
currentBuild.result = 'SUCCESS' // currentBuild.result = 'SUCCESS'
error("Stopping pipeline - no changes detected") // error("Stopping pipeline - no changes detected")
} // }
//
echo "Changes detected in smarketing-ai, proceeding with build" // echo "Changes detected in smarketing-ai, proceeding with build"
} // }
} // }
stage("Setup AKS") { stage("Setup AKS") {
container('azure-cli') { container('azure-cli') {

View File

@ -1380,7 +1380,7 @@ class SnsContentService:
'call_to_action': ['방문', '예약', '문의', '공감', '이웃추가'], 'call_to_action': ['방문', '예약', '문의', '공감', '이웃추가'],
'image_placement_strategy': [ 'image_placement_strategy': [
'매장 외관 → 인테리어 → 메뉴판 → 음식 → 분위기', '매장 외관 → 인테리어 → 메뉴판 → 음식 → 분위기',
'텍스트 2-3문장마다 이미지 배치', ##'텍스트 2-3문장마다 이미지 배치',
'이미지 설명은 간결하고 매력적으로', '이미지 설명은 간결하고 매력적으로',
'마지막에 대표 이미지로 마무리' '마지막에 대표 이미지로 마무리'
] ]
@ -1713,6 +1713,11 @@ class SnsContentService:
5. 줄바꿈을 활용하여 가독성 향상 5. 줄바꿈을 활용하여 가독성 향상
6. 해시태그는 본문과 자연스럽게 연결되도록 배치 6. 해시태그는 본문과 자연스럽게 연결되도록 배치
**이미지 태그 사용법:**
- [IMAGE_1]: 번째 이미지 배치 위치
- [IMAGE_2]: 번째 이미지 배치 위치
- 이미지 태그 다음 줄에 이미지 설명 문구 작성
**필수 요구사항:** **필수 요구사항:**
{request.requirement} or '고객의 관심을 끌고 방문을 유도하는 매력적인 게시물' {request.requirement} or '고객의 관심을 끌고 방문을 유도하는 매력적인 게시물'
@ -1777,21 +1782,14 @@ class SnsContentService:
1. 검색자의 궁금증을 해결하는 정보 중심 작성 1. 검색자의 궁금증을 해결하는 정보 중심 작성
2. 구체적인 가격, 위치, 운영시간 실용 정보 포함 2. 구체적인 가격, 위치, 운영시간 실용 정보 포함
3. 개인적인 경험과 솔직한 후기 작성 3. 개인적인 경험과 솔직한 후기 작성
4. 섹션마다 적절한 위치에 [IMAGE_X] 태그로 이미지 배치 위치 표시 4. 이미지마다 간단한 설명 문구 추가
5. 이미지마다 간단한 설명 문구 추가 5. 지역 정보와 접근성 정보 포함
6. 지역 정보와 접근성 정보 포함
**이미지 태그 사용법:**
- [IMAGE_1]: 번째 이미지 배치 위치
- [IMAGE_2]: 번째 이미지 배치 위치
- 이미지 태그 다음 줄에 이미지 설명 문구 작성
**필수 요구사항:** **필수 요구사항:**
{request.requirement} or '유용한 정보를 제공하여 방문을 유도하는 신뢰성 있는 후기' {request.requirement} or '유용한 정보를 제공하여 방문을 유도하는 신뢰성 있는 후기'
네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요. 네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요.
필수 요구사항을 반드시 참고하여 작성해주세요. 필수 요구사항을 반드시 참고하여 작성해주세요.
이미지 배치 위치를 [IMAGE_X] 태그로 명확히 표시해주세요.
""" """
return prompt return prompt

View File

@ -43,7 +43,11 @@ public class SecurityConfig {
.cors(cors -> cors.configurationSource(corsConfigurationSource())) .cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() .requestMatchers("/api/auth/**", "/api/member/register", "/api/member/check-duplicate/**",
"/api/member/validate-password", "/swagger-ui/**", "/v3/api-docs/**",
"/swagger-resources/**", "/webjars/**", "/actuator/**", "/health/**", "/error"
).permitAll()
.anyRequest().authenticated()
) )
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

View File

@ -43,22 +43,22 @@ podTemplate(
echo "Image Org: ${props.image_org}" echo "Image Org: ${props.image_org}"
} }
stage("Check Changes") { // stage("Check Changes") {
script { // script {
def changes = sh( // def changes = sh(
script: "git diff --name-only HEAD~1 HEAD", // script: "git diff --name-only HEAD~1 HEAD",
returnStdout: true // returnStdout: true
).trim() // ).trim()
//
if (!changes.contains("smarketing-java/")) { // if (!changes.contains("smarketing-java/")) {
echo "No changes in smarketing-java, skipping build" // echo "No changes in smarketing-java, skipping build"
currentBuild.result = 'SUCCESS' // currentBuild.result = 'SUCCESS'
error("Stopping pipeline - no changes detected") // error("Stopping pipeline - no changes detected")
} // }
//
echo "Changes detected in smarketing-java, proceeding with build" // echo "Changes detected in smarketing-java, proceeding with build"
} // }
} // }
stage("Setup AKS") { stage("Setup AKS") {
container('azure-cli') { container('azure-cli') {

View File

@ -182,29 +182,6 @@ spec:
name: common-secret name: common-secret
- secretRef: - secretRef:
name: member-secret name: member-secret
startupProbe:
tcpSocket:
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8081
initialDelaySeconds: 120
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8081
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
@ -248,29 +225,7 @@ spec:
name: common-secret name: common-secret
- secretRef: - secretRef:
name: store-secret name: store-secret
startupProbe:
tcpSocket:
port: 8082
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8082
initialDelaySeconds: 120
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8082
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
@ -314,29 +269,7 @@ spec:
name: common-secret name: common-secret
- secretRef: - secretRef:
name: marketing-content-secret name: marketing-content-secret
startupProbe:
tcpSocket:
port: 8083
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8083
initialDelaySeconds: 120
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8083
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
@ -380,29 +313,7 @@ spec:
name: common-secret name: common-secret
- secretRef: - secretRef:
name: ai-recommend-secret name: ai-recommend-secret
startupProbe:
tcpSocket:
port: 8084
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8084
initialDelaySeconds: 120
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8084
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
--- ---
# Services # Services
@ -462,84 +373,44 @@ spec:
type: ClusterIP type: ClusterIP
--- ---
# deploy.yaml.template의 Ingress 부분 - 완전한 설정
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: smarketing-backend name: smarketing-ingress
namespace: ${namespace}
annotations: annotations:
kubernetes.io/ingress.class: nginx kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /$2 # nginx.ingress.kubernetes.io/rewrite-target: /$2 # 이 줄 제거
nginx.ingress.kubernetes.io/ssl-redirect: "false" # nginx.ingress.kubernetes.io/use-regex: "true" # 이 줄 제거
nginx.ingress.kubernetes.io/use-regex: "true"
spec: spec:
ingressClassName: nginx
rules: rules:
- host: smarketing.20.249.184.228.nip.io - host: smarketing.20.249.184.228.nip.io
http: http:
paths: paths:
# Member 서비스 - 인증 관련 - path: /api/auth
- path: /api/auth(/|$)(.*) pathType: Prefix
pathType: ImplementationSpecific
backend: backend:
service: service:
name: member name: auth-service
port: port:
number: 80 number: 80
# Member 서비스 - 회원 관리 (누락된 경로!) - path: /api/store
- path: /api/member(/|$)(.*) pathType: Prefix
pathType: ImplementationSpecific
backend: backend:
service: service:
name: member name: store-service
port: port:
number: 80 number: 80
# Store 서비스 - path: /api/content
- path: /api/store(/|$)(.*) pathType: Prefix
pathType: ImplementationSpecific
backend: backend:
service: service:
name: store name: content-service
port: port:
number: 80 number: 80
# Store 서비스 - 매출 관련 - path: /api/recommend
- path: /api/sales(/|$)(.*) pathType: Prefix
pathType: ImplementationSpecific
backend: backend:
service: service:
name: store name: recommend-service
port:
number: 80
# Store 서비스 - 메뉴 관련
- path: /api/menu(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: store
port:
number: 80
# Store 서비스 - 이미지 업로드
- path: /api/images(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: store
port:
number: 80
# Marketing Content 서비스
- path: /api/content(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: marketing-content
port:
number: 80
# AI Recommend 서비스
- path: /api/recommend(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: ai-recommend
port: port:
number: 80 number: 80

View File

@ -9,7 +9,7 @@ image_org=smarketing
# Application Settings # Application Settings
replicas=1 replicas=1
allowed_origins=http://20.249.171.38 allowed_origins=http://20.249.154.194
# Security Settings # Security Settings
jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ

View File

@ -84,7 +84,7 @@ public class PosterContentService implements PosterContentUseCase {
// 콘텐츠 엔티티 생성 // 콘텐츠 엔티티 생성
Content content = Content.builder() Content content = Content.builder()
.contentType(ContentType.POSTER) .contentType(ContentType.POSTER)
.platform(Platform.GENERAL) .platform(Platform.POSTER)
.title(request.getTitle()) .title(request.getTitle())
.content(request.getContent()) .content(request.getContent())
.images(request.getImages()) .images(request.getImages())

View File

@ -14,6 +14,7 @@ import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse; import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest; import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -34,6 +35,9 @@ public class SnsContentService implements SnsContentUseCase {
private final AiContentGenerator aiContentGenerator; private final AiContentGenerator aiContentGenerator;
private final BlobStorageService blobStorageService; private final BlobStorageService blobStorageService;
@Value("${azure.storage.container.poster-images:content-images}")
private String contentImageContainer;
/** /**
* SNS 콘텐츠 생성 * SNS 콘텐츠 생성
* *
@ -44,8 +48,10 @@ public class SnsContentService implements SnsContentUseCase {
@Transactional @Transactional
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List<MultipartFile> files) { public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List<MultipartFile> files) {
//파일들 주소 가져옴 //파일들 주소 가져옴
List<String> urls = blobStorageService.uploadImage(files, "containerName"); if(files != null) {
List<String> urls = blobStorageService.uploadImage(files, contentImageContainer);
request.setImages(urls); request.setImages(urls);
}
// AI를 사용하여 SNS 콘텐츠 생성 // AI를 사용하여 SNS 콘텐츠 생성
String content = aiContentGenerator.generateSnsContent(request); String content = aiContentGenerator.generateSnsContent(request);
@ -67,8 +73,6 @@ public class SnsContentService implements SnsContentUseCase {
CreationConditions conditions = CreationConditions.builder() CreationConditions conditions = CreationConditions.builder()
.category(request.getCategory()) .category(request.getCategory())
.requirement(request.getRequirement()) .requirement(request.getRequirement())
//.toneAndManner(request.getToneAndManner())
//.emotionIntensity(request.getEmotionIntensity())
.eventName(request.getEventName()) .eventName(request.getEventName())
.startDate(request.getStartDate()) .startDate(request.getStartDate())
.endDate(request.getEndDate()) .endDate(request.getEndDate())
@ -76,7 +80,6 @@ public class SnsContentService implements SnsContentUseCase {
// 콘텐츠 엔티티 생성 저장 // 콘텐츠 엔티티 생성 저장
Content content = Content.builder() Content content = Content.builder()
// .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())

View File

@ -17,7 +17,7 @@ public enum Platform {
FACEBOOK("페이스북"), FACEBOOK("페이스북"),
KAKAO_STORY("카카오스토리"), KAKAO_STORY("카카오스토리"),
YOUTUBE("유튜브"), YOUTUBE("유튜브"),
GENERAL("일반"); POSTER("포스터");
private final String displayName; private final String displayName;

View File

@ -46,7 +46,7 @@ public class ContentController {
@Operation(summary = "SNS 게시물 생성", description = "AI를 활용하여 SNS 게시물을 생성합니다.") @Operation(summary = "SNS 게시물 생성", description = "AI를 활용하여 SNS 게시물을 생성합니다.")
@PostMapping(path = "/sns/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(path = "/sns/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<SnsContentCreateResponse>> generateSnsContent(@Valid @RequestPart("request") String requestJson, public ResponseEntity<ApiResponse<SnsContentCreateResponse>> generateSnsContent(@Valid @RequestPart("request") String requestJson,
@Valid @RequestPart("files") List<MultipartFile> images) throws JsonProcessingException { @Valid @RequestPart(name = "files", required = false) List<MultipartFile> images) throws JsonProcessingException {
SnsContentCreateRequest request = objectMapper.readValue(requestJson, SnsContentCreateRequest.class); SnsContentCreateRequest request = objectMapper.readValue(requestJson, SnsContentCreateRequest.class);
SnsContentCreateResponse response = snsContentUseCase.generateSnsContent(request, images); SnsContentCreateResponse response = snsContentUseCase.generateSnsContent(request, images);
return ResponseEntity.ok(ApiResponse.success(response, "SNS 콘텐츠가 성공적으로 생성되었습니다.")); return ResponseEntity.ok(ApiResponse.success(response, "SNS 콘텐츠가 성공적으로 생성되었습니다."));