From 5a7b1a60fc4e3e36c0810b92aabc9c4202376152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:14:47 +0900 Subject: [PATCH 01/33] add example --- smarketing-ai/services/sns_content_service.py | 1269 +++++++++++++++++ 1 file changed, 1269 insertions(+) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 24fa7a5..f3d06cd 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -16,6 +16,1271 @@ class SnsContentService: self.ai_client = AIClient() self.image_processor = ImageProcessor() + # 콘텐츠 글 예시 + self.contents_example = [ + { + "raw_html": """
+
+
+
+
+

팔공

중국음식하면 짬뽕이 제일 먼저 저는 떠오릅니다. 어릴 적 부터 짜장은 그닥 좋아하지 않았기에 지금도 짜장 보다는 짬뽕 그리고 볶음밥을 더 사랑합니다.(탕수육도 그닥 좋아하지는 않습니다) 지난 주말 11시30분쯤 갔다가 기겁(?)을 하고 일산으로 갔었던 기억이 납니다. 이날은 평일 조금 늦은 시간이기에 웨이팅이 없겠지 하고 갔습니다. 다행히 웨이팅은 없는데 홀에 딱 한자리가 있어서 다행히 착석을 하고 주문을 합니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

중화요리 팔공

위치안내: 서울 관악구 남부순환로 1680

영업시간: 11시 20분 ~ 21시 30분( 15시 ~ 17시 브레이크타임, 일요일 휴무)

메뉴: 짜장면, 해물짬뽕, 고기짬뽕, 볶음밥, 탕수육등

+
+
+
+
+
+
+
+
+

3명이 주문한 메뉴는 짜장면, 옛날볶음밥, 팔공해물짬뽕 2개 총 4가지 주문을 합니다.

+
+
+
+
+
+
+
+
+

+
+
+
+ +
+
+
+
50m
지도 데이터
x
© NAVER Corp. /OpenStreetMap
지도 확대
지도 확대/축소 슬라이더
지도 축소

지도 컨트롤러 범례

부동산
거리
읍,면,동
시,군,구
시,도
국가
+ +
+ + +
+
+ +
+
+
+
+

오랜만에 오셨네요 하셔서 " 이젠 와인 못 마시겠네요 "했더니 웃으시더군요 ㅎ

https://blog.naver.com/melburne/222278591313

+
+
+
+ +
+
+ + +
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

차림료

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

밑반찬들 ㅎ

요즘 짜사이 주는 곳 참 좋아합니다. 어디였더라? 짜사이가 엄청 맛있었던 곳이 얼마 전 있었는데 음.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

옛날볶음밥(12,000원)

불맛나고 고슬고슬 잘 볶아낸 볶음밥에 바로 볶아서 내어주는 짜장까지 정말이지 훌륭한 볶음밥입니다. 오랜만에 만나다보니 흥문을 ㅎ

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

고슬고슬 기름기 없이 볶아내서 내어주십니다. 3명이서 총 4개의 메뉴를 주문했습니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

후라이가 아쉽네요. 튀긴 옛날 후라이가 좋은데 아습입니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

이집 계란국도 헛투루 내어주지 않으십니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

짜장과 함께 먹는 볶음밥은 역시 굿입니다. 맛나네요.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

짜장면(10.000원)

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

일반짜장면이라고 하기보다는 채소도 큼직한 간짜장이라고 보시는 게 맞을 거 같습니다,.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

면에 짜장이 잘 베이면서 진득한게 끝내주죠. 저는 한 젓가락 조금 얻어서 맛을 봤는데 역시나 좋네요.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

팔공해물짬뽕(13,000원)

최근래 먹은 해물짬뽕 중에서 해산물이 제일 많이 들어 있다고 해야할까요? 큼직큼직하게 들어 있으면서 묵직한 듯 한게 눈으로만 봐도 '맛있겠구나' 라는 생각이 팍팍 들었습니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

처음 나온 볶음밥은 셋이서 맛나게 먹고 각자의 음식을 탐닉하기 시작합니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

탱글탱글한 해물들이 어짜피 냉동이겠지만 그래도 싱싱(?)한 듯 맛있습니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

면발도 좋고 캬~...

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

비싼(?)선동오징어도 푸짐하게 들어있네요. 대왕이, 솔방울 이런 거 없습니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

맛있는 짬뽕은 해산물부터 국물까지 다 맛있습니다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

줄을 서는 게 무서워서 국물 한방울 안남기고 클리어 했습니다. (국물이 구수하면서 적당히 묵직하고 정말 맛있습니다.)

+
+
+
+ +
+
+
+
+

최종평가: 올해 먹은 짬뽕 중 최고라고 감히 말을 할 수 있을 거 같습니다. 예전보다 더 맛있어 졌으니 사람이 더 많아졌겠죠. 참고로 옛날고기짬뽕은 1시30분전에 솔드아웃된다고 합니다.

+
+
+
+
+
+
+
+
+

+
+
+
+ +
""", + "title": "팔공", + "summary": "중화요리 맛집 리뷰" + }, + { + "raw_html": + """
+
+
+
+
+

[남천동 맛집] 안목 - 훌륭한 돼지국밥 한 그릇

미쉐린에 선택한 식당에 특별히 호감이 가는 것은 아니다.

하지만 궁금하기는 하다.

어떤 점에서 좋게 보고 선정을 한 것인지 궁금했다.

내가 가본 식당이라면 판단하면 되겠지만 가보지 않은 식당이라면 그 궁금증은 더 크다.

특히 가장 대중적인 음식이라면 더 클 것이다.

부산의 미쉐린 빕구르망에 2년 연속 선정한 돼지국밥집이 있다.

오가며 보기는 했지만 아직 가보진 못했다.

일부러 찾아가 보았다.

남천동의 "안목"이다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

정문 사진을 찍지 못해서 구글에서 하나 가져왔다. 밖에서 봐도 돼지국밥집 같아 보이지 않는다. 깔끔하고 모던하다.

남천동 등기소 바로 옆 건물이다. 주차장은 별도로 없으니 뒷골목의 주차장을 이용하여야 한다.

그런데 상호의 느낌은 일본풍같이 느껴진다. 혹시 그 뜻을 아시는 분들은 좀 알려주시면 고맙겠다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

좌석은 테이블은 없고 카운터석으로만 되어 있다. 최근 이름난 돼지국밥집들은 다 이런 식으로 만드는 것 같다.

전에 지나다 줄을 서는 것을 보았는데 이날 비가 와서 그랬는지 한가하다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

메뉴가 심플하다. 그냥 돼지국밥에 머릿고기 국밥 정도이다. 수육과 냉제육이 있는데 다음에 가게 되면 먹어보고 싶다.

가격은 비싸지 않은 것은 아닌데 더 비싸지 않아서 다행스럽다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

첨가할 수 있는 여러 가지

+
+
+
+ +
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ +
+
+
+
+

이런 것들이 있는데 마늘만 넣어 먹었다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

내가 주문한 머릿고기 국밥이다. 1인분씩 담겨 나온다.

+
+
+
+ +
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ +
+
+
+
+

머리 위의 선반에 쟁반이 올려져 있으면 그것을 내가 받아서 먹어야 한다. 반찬은 특별한 것은 없는데 이날 풋고추가 맛있었다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

굉장히 뽀얀 국물의 국밥이다. 머릿고기가 올려져 있다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

이것은 아내가 먹은 그냥 돼지국밥이다. 고기만 다른 국밥이다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

국밥에는 간이 되어 있어서 더 넣지 않아도 충분히 먹을 수 있었다. 그러니 다진 양념이나 새우젓은 맛을 보고 첨가하시길....

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

일본 라멘에 넣는 마늘을 짜서 넣는다. 하나 정도면 충분하겠다.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

맛있게 잘 먹었다.

맛있다. 쵸 근래 너무 저가의 돼지국밥만 먹고 다녀서인지 안목의 국밥은 맛있었다.

국물이 너무 무겁지도 않으면서도 진득했다.

완성도가 높다. 국물은 손가락에 꼽을 정도로 괜찮았다.

고기의 품질도 좋았고 손질도 잘했다. 부드럽고 또 비계 부분은 쫄깃했다.

다만 고기가 많아 보이지만 한 점 한 점이 굉장히 얇아서 무게로 치면 그렇게 많은 양은 아닐 것이다.

그리고 국밥 전체적으로 양은 그다지 많은 편은 아니다.

이 정도의 맛이면 미쉐린 빕구르망에 선정되는 것인지는 모르겠지만 나로서는 충분하다고 느껴진다.

내가 추구하는 수더분하고 푸짐한 국밥하고는 반대편에 있는 국밥이지만 완성도가 높으니 다 괜찮아 보인다.

좀 편하게 갈 수 있다면 가끔 가고 싶다.

서면과 부산역에 분점이 있다고 하니 그곳이 좀 편하겠다.

+
+
+
+ +
+
+
+
50m
지도 데이터
x
© NAVER Corp. /OpenStreetMap
지도 확대
지도 확대/축소 슬라이더
지도 축소

지도 컨트롤러 범례

부동산
거리
읍,면,동
시,군,구
시,도
국가
+ +
+ + +
+
+ +
+
+
+
+

+
+
+
+ +
""", + "title": "안목", + "summary": "국밥 맛집 리뷰" + }, + { + "raw_html": """
+
+
+
+
+ + + +
+
+
+
+
+
+
+

서울 미쉐린맛집 한식전문 목멱산방

-투쁠한우 육회비빔밥

-검은깨두부 보쌈

+
+
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

서울 중구 퇴계로20길 71

영업시간

매일

11:00 - 20:00

라스트오더

19:20

전화

02-318-4790

+
+
+
+
+
+
+
+
+

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

서울 남산은 참 묘한 매력이 있는 곳 같아요!

도시 속인데도 한 발짝만 올라오면

바람도 다르고, 공기도 다르고,

마음까지 탁 트이는 그런 느낌!

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

그런 남산 한켠에 있는

서울 미쉐린 맛집

목멱산방 본점에서

특별한 한 끼를 즐기고 왔어요!

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

식사 중간중간 보니 외국인 관광객도 많았고

데이트나 가족 외식으로 많이들 오더라고요~

실내는 군더더기 없이 깔끔하고

모던한 느낌이라

전통 한식을

더 세련되게 느낄 수 있어요.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

주문은 셀프 방식으로

키오스크로 하면돼요~

방송에도 여러번 나오고

미쉐린 맛집답게

주말에는 사람이 많아요!

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

이날 저희가 선택한 메뉴는

검은깨두부와 보쌈,

그리고

시그니처 메뉴인

투뿔한우 육회비빔밥을

주문했는데

기대 이상이었어요!

+
+
+
+ +
+
+
+
+

검은깨두부&보쌈

+
+
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

먼저 검은깨두부와 보쌈!!

검은깨 두부는

보기만 해도

고소한 향이 물씬 풍기는것같고

입에 넣자마자 사르르 녹아요!!

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

정말 진한 고소함이 입안에 퍼지는데,

이게 그냥 두부가 아니라는 걸

한입만 먹어도 느낄 수 있어요.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

그 두부와 함께 나오는 보쌈은

지방과 살코기 비율이 완벽해서

쫀득하면서도 부드러워요.

거기에 곁들여지는

볶음김치와 특제 야채무침이

보쌈 맛을 확 살려줘서,

딱 한식의 진수라는 말이

떠오르더라고요!

+
+
+
+ +
+
+
+
+

투쁠한우 육회비빔밥

+
+
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

대망의 투쁠한우 육회비빔밥!

비주얼도 예쁘고

정말 먹음직 스러웠어요!

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

이건 먼저 육회만 따로 맛봤는데,

신선한 투뿔 채끝살에

유자청과 꿀로 살짝 단맛을 더한

양념이 어우러져,

하나도 느끼하지 않고 깔끔했어요.!!

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

비빔밥은 나물과 함께

조심스럽게 비벼 한입 먹었을 때,

고추장을 넣지 않고도

양념된 육회와 참기름만으로

깊은 맛이 나는 게,

정말 재료 하나하나에

얼마나 정성을 들였는지

알겠더라고요.

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

비빔밥 안에 들어가는 나물도

건나물, 생야채, 표고버섯,

도라지, 고사리 등

제철에 맞춰 엄선된

나물들이 들어가는데,

하나하나 다 본연의 맛이 좋았어요~

+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+

삼광쌀로 지은 밥도 맛있더라구요~

밥 한 숟가락에

입안이 꽉 차는 느낌이 넘 좋았어요!

+
+
+
+ +
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+ +
+
+
+
+

함께 주문하고 싶은 사이드 메뉴는

바로 치즈김치전!

피자치즈와 모짜렐라가

가득 들어간 김치전인데,

겉은 바삭하고 속은 촉촉한 게

비빔밥이랑 궁합 최고예요.

+
+
+
+ +
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+ +
+
+
+
+

술 한잔 곁들이고 싶다면,

비빔밥 전용 막걸리도 있어요.

‘한 잔 막걸리’라는 이름답게

식전–식중–식후로 나눠 마시는 재미가 있어요.

과일향도 은은하고,

단맛과 신맛이 균형 잡혀 있어서

비빔밥과 찰떡이에요.

남산 산책하다가,

혹은 명동역 근처로

들리기 좋은 곳이랍니다^^

+
+
+
+ +
+
+
+
50m
지도 데이터
x
© NAVER Corp. /OpenStreetMap
지도 확대
지도 확대/축소 슬라이더
지도 축소

지도 컨트롤러 범례

부동산
거리
읍,면,동
시,군,구
시,도
국가
+ +
+ +
+
+ +
+
+
+
+

+
+
+
+ +
""", + "title": "목멱산방", + "summary": "한식 맛집 리뷰" + } + ] + + # 플랫폼별 콘텐츠 특성 정의 (대폭 개선) self.platform_specs = { '인스타그램': { @@ -137,6 +1402,10 @@ class SnsContentService: # 플랫폼별 특화 프롬프트 생성 prompt = self._create_platform_specific_prompt(request, image_analysis, image_placement_plan) + # blog_example을 프롬프트에 추가 + if hasattr(self, 'contents_example') and self.contents_example: + prompt += f"\n\n**참고 예시:**\n{str(self.contents_example)}\n위 예시를 참고하여 비슷한 스타일로 작성해주세요." + # AI로 콘텐츠 생성 generated_content = self.ai_client.generate_text(prompt, max_tokens=1500) From 1b9e00e9c62a182ed521f4f6772c31c123d7198d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:38:40 +0900 Subject: [PATCH 02/33] add example and insta pic --- smarketing-ai/app.py | 5 +- smarketing-ai/models/request_models.py | 5 +- smarketing-ai/services/sns_content_service.py | 161 +++++++++++++----- 3 files changed, 123 insertions(+), 48 deletions(-) diff --git a/smarketing-ai/app.py b/smarketing-ai/app.py index 6a4b9d4..5c197e5 100644 --- a/smarketing-ai/app.py +++ b/smarketing-ai/app.py @@ -73,8 +73,9 @@ def create_app(): platform=data.get('platform'), images=data.get('images', []), requirement=data.get('requirement'), - toneAndManner=data.get('toneAndManner'), - emotionIntensity=data.get('emotionIntensity'), + target=data.get('target'), + #toneAndManner=data.get('toneAndManner'), + #emotionIntensity=data.get('emotionIntensity'), menuName=data.get('menuName'), eventName=data.get('eventName'), startDate=data.get('startDate'), diff --git a/smarketing-ai/models/request_models.py b/smarketing-ai/models/request_models.py index 653dfb8..b74e70e 100644 --- a/smarketing-ai/models/request_models.py +++ b/smarketing-ai/models/request_models.py @@ -15,9 +15,10 @@ class SnsContentGetRequest: contentType: str platform: str images: List[str] # 이미지 URL 리스트 + target : Optional[str] = None # 타켓 requirement: Optional[str] = None - toneAndManner: Optional[str] = None - emotionIntensity: Optional[str] = None + #toneAndManner: Optional[str] = None + #emotionIntensity: Optional[str] = None menuName: Optional[str] = None eventName: Optional[str] = None startDate: Optional[date] = None # LocalDate -> date diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index f3d06cd..904c8c5 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -16,8 +16,8 @@ class SnsContentService: self.ai_client = AIClient() self.image_processor = ImageProcessor() - # 콘텐츠 글 예시 - self.contents_example = [ + # 블로그 글 예시 + self.blog_example = [ { "raw_html": """
@@ -517,7 +517,7 @@ class SnsContentService:
""", "title": "팔공", - "summary": "중화요리 맛집 리뷰" + "summary": "중화요리 맛집 홍보" }, { "raw_html": @@ -832,7 +832,7 @@ class SnsContentService:
""", "title": "안목", - "summary": "국밥 맛집 리뷰" + "summary": "국밥 맛집 홍보" }, { "raw_html": """
@@ -1276,11 +1276,65 @@ class SnsContentService:
""", "title": "목멱산방", - "summary": "한식 맛집 리뷰" + "summary": "한식 맛집 홍보" } ] + # 인스타 글 예시 + self.insta_example = [ + { + "caption": """힘든 월요일 잘 이겨내신 여러분~~~ + 소나기도 내리고 힘드셨을텐데 + 오늘 하루 고생 많으셨어요~~^^ + 고생한 나를 위해 시원한 맥주에 + 낙곱새~~기가 막히죠??낙지에 대창올리고 + 그 위에 새우~화룡점정으로 생와사비~ + 그 맛은 뭐 말씀 안드려도 여러분들이 + 더 잘 아실거예요~~그럼 다들 낙곱새 고고~~""", + "title": "국민 낙곱새", + "summary": "낙곱새 맛집 홍보" + }, + { + "caption": """안녕하세요! 타코몰리김포점입니다! + 타코몰리는 멕시코 문화와 풍부한맛을 경험할 수 있는 특별한 공간입니다.🎉 + + 🌶 대표 메뉴를 맛보세요 + 수제 타코, 바삭한 퀘사디아, 풍성한 부리또로 다양한 맛을 즐길 수 있습니다. + + 📸 특별한 순간을 담아보세요 + #타코몰리김포 해시태그와 함께 여러분의 멋진 사진을 공유해주세요. + 이벤트가 기다리고 있답니다!! + (새우링/치즈스틱/음료 택1) + + 📍 위치 + 김포한강 11로 140번길 15-2 + + 멕시코의 맛과 전통에 푹 빠져보세요! + 언제든지 여러분을 기다리고 있겠습니다🌟""", + "title": "타코몰리", + "summary": "멕시칸 맛집 홍보" + }, + { + "caption":"""📣명륜진사갈비 신메뉴 3종 출시! - + 특제 고추장 양념에 마늘과 청양고추를 더해 + 매콤한 불맛이 일품인 #매콤불고기 🌶️ + + 특제 간장 양념에 마늘과 청양고추를 더해 + 달콤한 감칠맛이 있는 #달콤불고기 🍯 + + 갈비뼈에 붙어있는 부위로 일반 삼겹살보다 + 더욱 깊은 맛과 풍미를 가진 #삼겹갈비 까지🍖 + + 신메뉴로 더욱 풍성해진 명륜진사갈비에서 + 연말 가족/단체모임을 즐겨보세요! + + ※ 신메뉴는 지점에 따라 탄력적으로 운영되고 있으니, + 자세한 문의는 방문하실 매장으로 확인 부탁드립니다.""", + "title": "명륜진사갈비", + "summary": "갈비 맛집 홍보" + } + ] + # 플랫폼별 콘텐츠 특성 정의 (대폭 개선) self.platform_specs = { '인스타그램': { @@ -1334,24 +1388,24 @@ class SnsContentService: } # 톤앤매너별 스타일 (플랫폼별 세분화) - self.tone_styles = { - '친근한': { - '인스타그램': '반말, 친구같은 느낌, 이모티콘 많이 사용', - '네이버 블로그': '존댓말이지만 따뜻하고 친근한 어조' - }, - '정중한': { - '인스타그램': '정중하지만 접근하기 쉬운 어조', - '네이버 블로그': '격식 있고 신뢰감 있는 리뷰 스타일' - }, - '재미있는': { - '인스타그램': '유머러스하고 트렌디한 표현', - '네이버 블로그': '재미있는 에피소드가 포함된 후기' - }, - '전문적인': { - '인스타그램': '전문성을 어필하되 딱딱하지 않게', - '네이버 블로그': '전문가 관점의 상세한 분석과 평가' - } - } + # self.tone_styles = { + # '친근한': { + # '인스타그램': '반말, 친구같은 느낌, 이모티콘 많이 사용', + # '네이버 블로그': '존댓말이지만 따뜻하고 친근한 어조' + # }, + # '정중한': { + # '인스타그램': '정중하지만 접근하기 쉬운 어조', + # '네이버 블로그': '격식 있고 신뢰감 있는 리뷰 스타일' + # }, + # '재미있는': { + # '인스타그램': '유머러스하고 트렌디한 표현', + # '네이버 블로그': '재미있는 에피소드가 포함된 후기' + # }, + # '전문적인': { + # '인스타그램': '전문성을 어필하되 딱딱하지 않게', + # '네이버 블로그': '전문가 관점의 상세한 분석과 평가' + # } + # } # 카테고리별 플랫폼 특화 키워드 self.category_keywords = { @@ -1370,11 +1424,11 @@ class SnsContentService: } # 감정 강도별 표현 - self.emotion_levels = { - '약함': '은은하고 차분한 표현', - '보통': '적당히 활기찬 표현', - '강함': '매우 열정적이고 강렬한 표현' - } + # self.emotion_levels = { + # '약함': '은은하고 차분한 표현', + # '보통': '적당히 활기찬 표현', + # '강함': '매우 열정적이고 강렬한 표현' + # } # 이미지 타입 분류를 위한 키워드 self.image_type_keywords = { @@ -1403,8 +1457,10 @@ class SnsContentService: prompt = self._create_platform_specific_prompt(request, image_analysis, image_placement_plan) # blog_example을 프롬프트에 추가 - if hasattr(self, 'contents_example') and self.contents_example: - prompt += f"\n\n**참고 예시:**\n{str(self.contents_example)}\n위 예시를 참고하여 비슷한 스타일로 작성해주세요." + if request.platform == '네이버 블로그' and hasattr(self, 'blog_example') and self.blog_example: + prompt += f"\n\n**참고 예시:**\n{str(self.blog_example)}\n위 예시를 참고하여 점주의 입장에서 가게 홍보 게시물을 작성해주세요." + elif hasattr(self, 'insta_example') and self.insta_example : + prompt += f"\n\n**참고 예시:**\n{str(self.insta_example)}\n위 예시를 참고하여 점주의 입장에서 가게 홍보 게시물을 작성해주세요." # AI로 콘텐츠 생성 generated_content = self.ai_client.generate_text(prompt, max_tokens=1500) @@ -1594,7 +1650,7 @@ class SnsContentService: 플랫폼별 특화 프롬프트 생성 """ platform_spec = self.platform_specs.get(request.platform, self.platform_specs['인스타그램']) - tone_style = self.tone_styles.get(request.toneAndManner, {}).get(request.platform, '친근하고 자연스러운 어조') + #tone_style = self.tone_styles.get(request.toneAndManner, {}).get(request.platform, '친근하고 자연스러운 어조') # 이미지 설명 추출 image_descriptions = [] @@ -1604,14 +1660,14 @@ class SnsContentService: # 플랫폼별 특화 프롬프트 생성 if request.platform == '인스타그램': - return self._create_instagram_prompt(request, platform_spec, tone_style, image_descriptions) + return self._create_instagram_prompt(request, platform_spec, image_descriptions) elif request.platform == '네이버 블로그': - return self._create_naver_blog_prompt(request, platform_spec, tone_style, image_descriptions, + return self._create_naver_blog_prompt(request, platform_spec, image_descriptions, image_placement_plan) else: - return self._create_instagram_prompt(request, platform_spec, tone_style, image_descriptions) + return self._create_instagram_prompt(request, platform_spec, image_descriptions) - def _create_instagram_prompt(self, request: SnsContentGetRequest, platform_spec: dict, tone_style: str, + def _create_instagram_prompt(self, request: SnsContentGetRequest, platform_spec: dict, image_descriptions: list) -> str: """ 인스타그램 특화 프롬프트 @@ -1627,12 +1683,12 @@ class SnsContentService: - 콘텐츠 타입: {request.contentType} - 메뉴명: {request.menuName or '특별 메뉴'} - 이벤트: {request.eventName or '특별 이벤트'} +- 독자층: {request.target} **📱 인스타그램 특화 요구사항:** - 글 구조: {platform_spec['content_structure']} - 최대 길이: {platform_spec['max_length']}자 - 해시태그: {platform_spec['hashtag_count']}개 내외 -- 톤앤매너: {tone_style} **✨ 인스타그램 작성 가이드라인:** {chr(10).join([f"- {tip}" for tip in platform_spec['writing_tips']])} @@ -1654,14 +1710,16 @@ class SnsContentService: 5. 줄바꿈을 활용하여 가독성 향상 6. 해시태그는 본문과 자연스럽게 연결되도록 배치 -**특별 요구사항:** -{request.requirement or '고객의 관심을 끌고 방문을 유도하는 매력적인 게시물'} +**필수 요구사항:** +{request.requirement #or '고객의 관심을 끌고 방문을 유도하는 매력적인 게시물' +} 인스타그램 사용자들이 "저장하고 싶다", "친구에게 공유하고 싶다"라고 생각할 만한 매력적인 게시물을 작성해주세요. +필수 요구사항을 반드시 참고하여 작성해주세요. """ return prompt - def _create_naver_blog_prompt(self, request: SnsContentGetRequest, platform_spec: dict, tone_style: str, + def _create_naver_blog_prompt(self, request: SnsContentGetRequest, platform_spec: dict, image_descriptions: list, image_placement_plan: Dict[str, Any]) -> str: """ 네이버 블로그 특화 프롬프트 (이미지 배치 계획 포함) @@ -1690,11 +1748,11 @@ class SnsContentService: - 콘텐츠 타입: {request.contentType} - 메뉴명: {request.menuName or '대표 메뉴'} - 이벤트: {request.eventName or '특별 이벤트'} +- 독자층: {request.target} **🔍 네이버 블로그 특화 요구사항:** - 글 구조: {platform_spec['content_structure']} - 최대 길이: {platform_spec['max_length']}자 -- 톤앤매너: {tone_style} - SEO 최적화 필수 **📚 블로그 작성 가이드라인:** @@ -1728,10 +1786,13 @@ class SnsContentService: - [IMAGE_2]: 두 번째 이미지 배치 위치 - 각 이미지 태그 다음 줄에 이미지 설명 문구 작성 -**특별 요구사항:** -{request.requirement or '유용한 정보를 제공하여 방문을 유도하는 신뢰성 있는 후기'} +**필수 요구사항:** +{request.requirement + # or '유용한 정보를 제공하여 방문을 유도하는 신뢰성 있는 후기' +} 네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요. +필수 요구사항을 반드시 참고하여 작성해주세요. 이미지 배치 위치를 [IMAGE_X] 태그로 명확히 표시해주세요. """ return prompt @@ -1796,8 +1857,20 @@ class SnsContentService: # 1. literal \n 문자열을 실제 줄바꿈으로 변환 content = content.replace('\\n', '\n') + # 2. 인스타그램인 경우 첫 번째 이미지를 맨 위에 배치 ⭐ 새로 추가! + images_html_content = "" + if request.platform == '인스타그램' and request.images and len(request.images) > 0: + # 모든 이미지를 통일된 크기로 HTML 변환 (한 줄로 작성!) + for i, image_url in enumerate(request.images): + # ⭐ 핵심: 모든 HTML을 한 줄로 작성해서
변환 문제 방지 + image_html = f'
이미지 {i + 1}
' + images_html_content += image_html + "\n" + + # 이미지를 콘텐츠 맨 앞에 추가 + content = images_html_content + content + # 2. 네이버 블로그인 경우 이미지 태그를 실제 이미지로 변환 - if request.platform == '네이버 블로그' and image_placement_plan: + elif request.platform == '네이버 블로그' and image_placement_plan: content = self._replace_image_tags_with_html(content, image_placement_plan, request.images) # 3. 실제 줄바꿈을
태그로 변환 From f83269e5f619f7bdb0ee5a2ad212eca77710ec3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:51:13 +0900 Subject: [PATCH 03/33] add store info --- smarketing-ai/app.py | 2 ++ smarketing-ai/models/request_models.py | 2 ++ smarketing-ai/services/sns_content_service.py | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/smarketing-ai/app.py b/smarketing-ai/app.py index 5c197e5..2aa393a 100644 --- a/smarketing-ai/app.py +++ b/smarketing-ai/app.py @@ -73,6 +73,8 @@ def create_app(): platform=data.get('platform'), images=data.get('images', []), requirement=data.get('requirement'), + storeName=data.get('storeName'), + storeType=data.get('storeType'), target=data.get('target'), #toneAndManner=data.get('toneAndManner'), #emotionIntensity=data.get('emotionIntensity'), diff --git a/smarketing-ai/models/request_models.py b/smarketing-ai/models/request_models.py index b74e70e..5f08f54 100644 --- a/smarketing-ai/models/request_models.py +++ b/smarketing-ai/models/request_models.py @@ -17,6 +17,8 @@ class SnsContentGetRequest: images: List[str] # 이미지 URL 리스트 target : Optional[str] = None # 타켓 requirement: Optional[str] = None + storeName: Optional[str] = None + storeType: Optional[str] = None #toneAndManner: Optional[str] = None #emotionIntensity: Optional[str] = None menuName: Optional[str] = None diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 904c8c5..c8605c8 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1676,6 +1676,9 @@ class SnsContentService: prompt = f""" 당신은 인스타그램 마케팅 전문가입니다. 소상공인 음식점을 위한 매력적인 인스타그램 게시물을 작성해주세요. +**🍸 가게 정보:** +- 가게명: {request.storeName} +- 업종 : {request.storeType} **🎯 콘텐츠 정보:** - 제목: {request.title} @@ -1742,6 +1745,10 @@ class SnsContentService: prompt = f""" 당신은 네이버 블로그 맛집 리뷰 전문가입니다. 검색 최적화와 정보 제공을 중시하는 네이버 블로그 특성에 맞는 게시물을 작성해주세요. +**🍸 가게 정보:** +- 가게명: {request.storeName} +- 업종 : {request.storeType} + **📝 콘텐츠 정보:** - 제목: {request.title} - 카테고리: {request.category} From ead0a9833d08e615e6e975a502096b3c5c727f66 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 15:58:11 +0900 Subject: [PATCH 04/33] Create Jenkinsfile --- smarketing-java/member/Jenkinsfile | 81 ++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 smarketing-java/member/Jenkinsfile diff --git a/smarketing-java/member/Jenkinsfile b/smarketing-java/member/Jenkinsfile new file mode 100644 index 0000000..3267d52 --- /dev/null +++ b/smarketing-java/member/Jenkinsfile @@ -0,0 +1,81 @@ +pipeline { + agent any + + environment { + ACR_LOGIN_SERVER = 'acrsmarketing17567.azurecr.io' + IMAGE_NAME = 'member' + MANIFEST_REPO = 'https://github.com/won-ktds/smarketing-manifest.git' + MANIFEST_PATH = 'member/deployment.yaml' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Build') { + steps { + dir('member') { + sh './gradlew clean build -x test' + } + } + } + + stage('Test') { + steps { + dir('member') { + sh './gradlew test' + } + } + } + + stage('Build Docker Image') { + steps { + script { + def imageTag = "${BUILD_NUMBER}-${env.GIT_COMMIT.substring(0,8)}" + def fullImageName = "${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${imageTag}" + + dir('member') { + sh "docker build -t ${fullImageName} ." + } + + withCredentials([usernamePassword(credentialsId: 'acr-credentials', usernameVariable: 'ACR_USERNAME', passwordVariable: 'ACR_PASSWORD')]) { + sh "docker login ${ACR_LOGIN_SERVER} -u ${ACR_USERNAME} -p ${ACR_PASSWORD}" + sh "docker push ${fullImageName}" + } + + env.IMAGE_TAG = imageTag + env.FULL_IMAGE_NAME = fullImageName + } + } + } + + stage('Update Manifest') { + steps { + withCredentials([usernamePassword(credentialsId: 'github-credentials', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_TOKEN')]) { + sh ''' + git clone https://${GIT_TOKEN}@github.com/won-ktds/smarketing-manifest.git manifest-repo + cd manifest-repo + + # Update image tag in deployment.yaml + sed -i "s|image: .*|image: ${FULL_IMAGE_NAME}|g" ${MANIFEST_PATH} + + git config user.email "jenkins@smarketing.com" + git config user.name "Jenkins" + git add . + git commit -m "Update ${IMAGE_NAME} image to ${IMAGE_TAG}" + git push origin main + ''' + } + } + } + } + + post { + always { + cleanWs() + } + } +} From 3bc9213dd3f1080795ca721f05299e31fad39937 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 15:59:43 +0900 Subject: [PATCH 05/33] Create Jenkinsfile --- Jenkinsfile | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..3267d52 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,81 @@ +pipeline { + agent any + + environment { + ACR_LOGIN_SERVER = 'acrsmarketing17567.azurecr.io' + IMAGE_NAME = 'member' + MANIFEST_REPO = 'https://github.com/won-ktds/smarketing-manifest.git' + MANIFEST_PATH = 'member/deployment.yaml' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Build') { + steps { + dir('member') { + sh './gradlew clean build -x test' + } + } + } + + stage('Test') { + steps { + dir('member') { + sh './gradlew test' + } + } + } + + stage('Build Docker Image') { + steps { + script { + def imageTag = "${BUILD_NUMBER}-${env.GIT_COMMIT.substring(0,8)}" + def fullImageName = "${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${imageTag}" + + dir('member') { + sh "docker build -t ${fullImageName} ." + } + + withCredentials([usernamePassword(credentialsId: 'acr-credentials', usernameVariable: 'ACR_USERNAME', passwordVariable: 'ACR_PASSWORD')]) { + sh "docker login ${ACR_LOGIN_SERVER} -u ${ACR_USERNAME} -p ${ACR_PASSWORD}" + sh "docker push ${fullImageName}" + } + + env.IMAGE_TAG = imageTag + env.FULL_IMAGE_NAME = fullImageName + } + } + } + + stage('Update Manifest') { + steps { + withCredentials([usernamePassword(credentialsId: 'github-credentials', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_TOKEN')]) { + sh ''' + git clone https://${GIT_TOKEN}@github.com/won-ktds/smarketing-manifest.git manifest-repo + cd manifest-repo + + # Update image tag in deployment.yaml + sed -i "s|image: .*|image: ${FULL_IMAGE_NAME}|g" ${MANIFEST_PATH} + + git config user.email "jenkins@smarketing.com" + git config user.name "Jenkins" + git add . + git commit -m "Update ${IMAGE_NAME} image to ${IMAGE_TAG}" + git push origin main + ''' + } + } + } + } + + post { + always { + cleanWs() + } + } +} From eff426ade65beac3cc0af80e43c74477577b9f4f Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Mon, 16 Jun 2025 16:08:14 +0900 Subject: [PATCH 06/33] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20LoadBalancer=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smarketing-ai/deployment/manifest/service.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-ai/deployment/manifest/service.yaml b/smarketing-ai/deployment/manifest/service.yaml index 87ba6f0..08dc1e8 100644 --- a/smarketing-ai/deployment/manifest/service.yaml +++ b/smarketing-ai/deployment/manifest/service.yaml @@ -6,7 +6,7 @@ metadata: labels: app: smarketing spec: - type: ClusterIP + type: LoadBalancer ports: - port: 5001 targetPort: 5001 From 5c07cf52982174e9bca757dcb6a85d95bbef13b4 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 16:25:36 +0900 Subject: [PATCH 07/33] Delete Jenkinsfile --- Jenkinsfile | 81 ----------------------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 3267d52..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,81 +0,0 @@ -pipeline { - agent any - - environment { - ACR_LOGIN_SERVER = 'acrsmarketing17567.azurecr.io' - IMAGE_NAME = 'member' - MANIFEST_REPO = 'https://github.com/won-ktds/smarketing-manifest.git' - MANIFEST_PATH = 'member/deployment.yaml' - } - - stages { - stage('Checkout') { - steps { - checkout scm - } - } - - stage('Build') { - steps { - dir('member') { - sh './gradlew clean build -x test' - } - } - } - - stage('Test') { - steps { - dir('member') { - sh './gradlew test' - } - } - } - - stage('Build Docker Image') { - steps { - script { - def imageTag = "${BUILD_NUMBER}-${env.GIT_COMMIT.substring(0,8)}" - def fullImageName = "${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${imageTag}" - - dir('member') { - sh "docker build -t ${fullImageName} ." - } - - withCredentials([usernamePassword(credentialsId: 'acr-credentials', usernameVariable: 'ACR_USERNAME', passwordVariable: 'ACR_PASSWORD')]) { - sh "docker login ${ACR_LOGIN_SERVER} -u ${ACR_USERNAME} -p ${ACR_PASSWORD}" - sh "docker push ${fullImageName}" - } - - env.IMAGE_TAG = imageTag - env.FULL_IMAGE_NAME = fullImageName - } - } - } - - stage('Update Manifest') { - steps { - withCredentials([usernamePassword(credentialsId: 'github-credentials', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_TOKEN')]) { - sh ''' - git clone https://${GIT_TOKEN}@github.com/won-ktds/smarketing-manifest.git manifest-repo - cd manifest-repo - - # Update image tag in deployment.yaml - sed -i "s|image: .*|image: ${FULL_IMAGE_NAME}|g" ${MANIFEST_PATH} - - git config user.email "jenkins@smarketing.com" - git config user.name "Jenkins" - git add . - git commit -m "Update ${IMAGE_NAME} image to ${IMAGE_TAG}" - git push origin main - ''' - } - } - } - } - - post { - always { - cleanWs() - } - } -} From 111156afb68ed6d0b5619cd49f2e8aac36e81ca2 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 16:26:51 +0900 Subject: [PATCH 08/33] Create deploy.yaml.template --- .../deployment/deploy.yaml.template | 475 ++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100644 smarketing-java/deployment/deploy.yaml.template diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template new file mode 100644 index 0000000..4b88867 --- /dev/null +++ b/smarketing-java/deployment/deploy.yaml.template @@ -0,0 +1,475 @@ +# ConfigMap +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: ${namespace} +data: + ALLOWED_ORIGINS: ${allowed_origins} + JPA_DDL_AUTO: update + JPA_SHOW_SQL: 'true' + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: member-config + namespace: ${namespace} +data: + POSTGRES_DB: member + POSTGRES_HOST: member-postgresql + POSTGRES_PORT: '5432' + SERVER_PORT: '8081' + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: store-config + namespace: ${namespace} +data: + POSTGRES_DB: store + POSTGRES_HOST: store-postgresql + POSTGRES_PORT: '5432' + SERVER_PORT: '8082' + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: marketing-content-config + namespace: ${namespace} +data: + POSTGRES_DB: marketing_content + POSTGRES_HOST: marketing-content-postgresql + POSTGRES_PORT: '5432' + SERVER_PORT: '8083' + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: ai-recommend-config + namespace: ${namespace} +data: + POSTGRES_DB: ai_recommend + POSTGRES_HOST: ai-recommend-postgresql + POSTGRES_PORT: '5432' + SERVER_PORT: '8084' + +--- +# Secrets +apiVersion: v1 +kind: Secret +metadata: + name: common-secret + namespace: ${namespace} +stringData: + JWT_SECRET_KEY: ${jwt_secret_key} +type: Opaque + +--- +apiVersion: v1 +kind: Secret +metadata: + name: member-secret + namespace: ${namespace} +stringData: + JWT_ACCESS_TOKEN_VALIDITY: '3600000' + JWT_REFRESH_TOKEN_VALIDITY: '86400000' + POSTGRES_PASSWORD: ${postgres_password} + POSTGRES_USER: ${postgres_user} +type: Opaque + +--- +apiVersion: v1 +kind: Secret +metadata: + name: store-secret + namespace: ${namespace} +stringData: + POSTGRES_PASSWORD: ${postgres_password} + POSTGRES_USER: ${postgres_user} +type: Opaque + +--- +apiVersion: v1 +kind: Secret +metadata: + name: marketing-content-secret + namespace: ${namespace} +stringData: + POSTGRES_PASSWORD: ${postgres_password} + POSTGRES_USER: ${postgres_user} +type: Opaque + +--- +apiVersion: v1 +kind: Secret +metadata: + name: ai-recommend-secret + namespace: ${namespace} +stringData: + POSTGRES_PASSWORD: ${postgres_password} + POSTGRES_USER: ${postgres_user} +type: Opaque + +--- +# Deployments +apiVersion: apps/v1 +kind: Deployment +metadata: + name: member + namespace: ${namespace} + labels: + app: member +spec: + replicas: ${replicas} + selector: + matchLabels: + app: member + template: + metadata: + labels: + app: member + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: member + image: ${member_image_path} + imagePullPolicy: Always + ports: + - containerPort: 8081 + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + envFrom: + - configMapRef: + name: common-config + - configMapRef: + name: member-config + - secretRef: + name: common-secret + - secretRef: + name: member-secret + startupProbe: + exec: + command: + - /bin/sh + - -c + - "nc -z member-postgresql 5432" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 10 + livenessProbe: + httpGet: + path: /actuator/health + port: 8081 + initialDelaySeconds: 60 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /actuator/health + port: 8081 + initialDelaySeconds: 30 + periodSeconds: 5 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: store + namespace: ${namespace} + labels: + app: store +spec: + replicas: ${replicas} + selector: + matchLabels: + app: store + template: + metadata: + labels: + app: store + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: store + image: ${store_image_path} + imagePullPolicy: Always + ports: + - containerPort: 8082 + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + envFrom: + - configMapRef: + name: common-config + - configMapRef: + name: store-config + - secretRef: + name: common-secret + - secretRef: + name: store-secret + startupProbe: + exec: + command: + - /bin/sh + - -c + - "nc -z store-postgresql 5432" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 10 + livenessProbe: + httpGet: + path: /actuator/health + port: 8082 + initialDelaySeconds: 60 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /actuator/health + port: 8082 + initialDelaySeconds: 30 + periodSeconds: 5 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: marketing-content + namespace: ${namespace} + labels: + app: marketing-content +spec: + replicas: ${replicas} + selector: + matchLabels: + app: marketing-content + template: + metadata: + labels: + app: marketing-content + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: marketing-content + image: ${marketing_content_image_path} + imagePullPolicy: Always + ports: + - containerPort: 8083 + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + envFrom: + - configMapRef: + name: common-config + - configMapRef: + name: marketing-content-config + - secretRef: + name: common-secret + - secretRef: + name: marketing-content-secret + startupProbe: + exec: + command: + - /bin/sh + - -c + - "nc -z marketing-content-postgresql 5432" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 10 + livenessProbe: + httpGet: + path: /actuator/health + port: 8083 + initialDelaySeconds: 60 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /actuator/health + port: 8083 + initialDelaySeconds: 30 + periodSeconds: 5 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ai-recommend + namespace: ${namespace} + labels: + app: ai-recommend +spec: + replicas: ${replicas} + selector: + matchLabels: + app: ai-recommend + template: + metadata: + labels: + app: ai-recommend + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: ai-recommend + image: ${ai_recommend_image_path} + imagePullPolicy: Always + ports: + - containerPort: 8084 + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + envFrom: + - configMapRef: + name: common-config + - configMapRef: + name: ai-recommend-config + - secretRef: + name: common-secret + - secretRef: + name: ai-recommend-secret + startupProbe: + exec: + command: + - /bin/sh + - -c + - "nc -z ai-recommend-postgresql 5432" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 10 + livenessProbe: + httpGet: + path: /actuator/health + port: 8084 + initialDelaySeconds: 60 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /actuator/health + port: 8084 + initialDelaySeconds: 30 + periodSeconds: 5 + +--- +# Services +apiVersion: v1 +kind: Service +metadata: + name: member + namespace: ${namespace} +spec: + selector: + app: member + ports: + - port: 80 + targetPort: 8081 + type: ClusterIP + +--- +apiVersion: v1 +kind: Service +metadata: + name: store + namespace: ${namespace} +spec: + selector: + app: store + ports: + - port: 80 + targetPort: 8082 + type: ClusterIP + +--- +apiVersion: v1 +kind: Service +metadata: + name: marketing-content + namespace: ${namespace} +spec: + selector: + app: marketing-content + ports: + - port: 80 + targetPort: 8083 + type: ClusterIP + +--- +apiVersion: v1 +kind: Service +metadata: + name: ai-recommend + namespace: ${namespace} +spec: + selector: + app: ai-recommend + ports: + - port: 80 + targetPort: 8084 + type: ClusterIP + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: smarketing-backend + namespace: ${namespace} + annotations: + kubernetes.io/ingress.class: nginx +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /api/auth + pathType: Prefix + backend: + service: + name: member + port: + number: 80 + - path: /api/store + pathType: Prefix + backend: + service: + name: store + port: + number: 80 + - path: /api/content + pathType: Prefix + backend: + service: + name: marketing-content + port: + number: 80 + - path: /api/recommend + pathType: Prefix + backend: + service: + name: ai-recommend + port: + number: 80 From 6cf003c10968d986308ed547f01f2e6b19c4be6a Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 16:27:10 +0900 Subject: [PATCH 09/33] Create deploy_env_vars --- smarketing-java/deployment/deploy_env_vars | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 smarketing-java/deployment/deploy_env_vars diff --git a/smarketing-java/deployment/deploy_env_vars b/smarketing-java/deployment/deploy_env_vars new file mode 100644 index 0000000..3bcdfa8 --- /dev/null +++ b/smarketing-java/deployment/deploy_env_vars @@ -0,0 +1,23 @@ +# Team Settings +teamid=kros235 +root_project=smarketing-backend +namespace=kros235-lifesub-ns + +# Container Registry Settings +registry=unicorncr.azurecr.io +image_org=smarketing + +# Application Settings +replicas=1 +allowed_origins=http://20.249.171.38 + +# Security Settings +jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ +postgres_user=admin +postgres_password=Hi5Jessica! + +# Resource Settings +resources_requests_cpu=256m +resources_requests_memory=256Mi +resources_limits_cpu=1024m +resources_limits_memory=1024Mi From e3d9c6db75811d1958a17f3a005000251a4b0899 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 16:27:53 +0900 Subject: [PATCH 10/33] Update deploy_env_vars --- smarketing-java/deployment/deploy_env_vars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-java/deployment/deploy_env_vars b/smarketing-java/deployment/deploy_env_vars index 3bcdfa8..fe5de69 100644 --- a/smarketing-java/deployment/deploy_env_vars +++ b/smarketing-java/deployment/deploy_env_vars @@ -1,7 +1,7 @@ # Team Settings teamid=kros235 root_project=smarketing-backend -namespace=kros235-lifesub-ns +namespace=smarketing # Container Registry Settings registry=unicorncr.azurecr.io From cc1a14df2d3ecd90088a9e6696e672108797d67a Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 16:28:17 +0900 Subject: [PATCH 11/33] Create Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 157 +++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 smarketing-java/deployment/Jenkinsfile diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile new file mode 100644 index 0000000..1b0910f --- /dev/null +++ b/smarketing-java/deployment/Jenkinsfile @@ -0,0 +1,157 @@ +def PIPELINE_ID = "${env.BUILD_NUMBER}" + +def getImageTag() { + def dateFormat = new java.text.SimpleDateFormat('yyyyMMddHHmmss') + def currentDate = new Date() + return dateFormat.format(currentDate) +} + +podTemplate( + label: "${PIPELINE_ID}", + serviceAccount: 'jenkins', + containers: [ + containerTemplate(name: 'podman', image: "mgoltzsche/podman", ttyEnabled: true, command: 'cat', privileged: true), + containerTemplate(name: 'gradle', + image: 'gradle:jdk17', + ttyEnabled: true, + command: 'cat', + envVars: [ + envVar(key: 'DOCKER_HOST', value: 'unix:///run/podman/podman.sock'), + envVar(key: 'TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE', value: '/run/podman/podman.sock'), + envVar(key: 'TESTCONTAINERS_RYUK_DISABLED', value: 'true') + ]), + containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true), + containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h') + ], + volumes: [ + emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false), + emptyDirVolume(mountPath: '/root/.azure', memory: false), + emptyDirVolume(mountPath: '/run/podman', memory: false) + ] +) { + node(PIPELINE_ID) { + def props + def imageTag = getImageTag() + def manifest = "deploy.yaml" + def namespace + def services = ['member', 'store', 'marketing-content', 'ai-recommend'] + + stage("Get Source") { + checkout scm + props = readProperties file: "deployment/deploy_env_vars" + namespace = "${props.namespace}" + } + + stage('Code Analysis & Quality Gate') { + container('gradle') { + sh "./gradlew clean build -x test" + + // SonarQube 분석 + withSonarQubeEnv('SonarQube Server') { + sh """ + ./gradlew sonarqube \\ + -Dsonar.projectKey=smarketing-backend-member-kros235 \\ + -Dsonar.host.url=\$SONAR_HOST_URL \\ + -Dsonar.login=\$SONAR_AUTH_TOKEN + + ./gradlew sonarqube \\ + -Dsonar.projectKey=smarketing-backend-store-kros235 \\ + -Dsonar.host.url=\$SONAR_HOST_URL \\ + -Dsonar.login=\$SONAR_AUTH_TOKEN + + ./gradlew sonarqube \\ + -Dsonar.projectKey=smarketing-backend-marketing-content-kros235 \\ + -Dsonar.host.url=\$SONAR_HOST_URL \\ + -Dsonar.login=\$SONAR_AUTH_TOKEN + + ./gradlew sonarqube \\ + -Dsonar.projectKey=smarketing-backend-ai-recommend-kros235 \\ + -Dsonar.host.url=\$SONAR_HOST_URL \\ + -Dsonar.login=\$SONAR_AUTH_TOKEN + """ + } + + // Quality Gate 확인 + timeout(time: 5, unit: 'MINUTES') { + waitForQualityGate abortPipeline: true + } + } + } + + stage('Build Application') { + container('gradle') { + sh """ + ./gradlew :member:clean :member:build -x test + ./gradlew :store:clean :store:build -x test + ./gradlew :marketing-content:clean :marketing-content:build -x test + ./gradlew :ai-recommend:clean :ai-recommend:build -x test + """ + } + } + + stage('Build & Push Images') { + container('podman') { + services.each { service -> + script { + // mysub 서비스 특별 처리 (요구사항에 없지만 참고용 주석) + // def buildDir = service == 'mysub' ? 'mysub-infra' : service + def buildDir = service + def jarFile = "${service}.jar" + + sh """ + podman build \\ + --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ + --build-arg ARTIFACTORY_FILE="${jarFile}" \\ + -f deployment/container/Dockerfile \\ + -t ${props.registry}/${props.image_org}/${service}:${imageTag} . + + podman push ${props.registry}/${props.image_org}/${service}:${imageTag} + """ + } + } + } + } + + stage('Generate & Apply Manifest') { + container('envsubst') { + sh """ + export namespace=${namespace} + export allowed_origins=${props.allowed_origins} + export jwt_secret_key=${props.jwt_secret_key} + export postgres_user=${props.postgres_user} + export postgres_password=${props.postgres_password} + export replicas=${props.replicas} + export resources_requests_cpu=${props.resources_requests_cpu} + export resources_requests_memory=${props.resources_requests_memory} + export resources_limits_cpu=${props.resources_limits_cpu} + export resources_limits_memory=${props.resources_limits_memory} + + # 이미지 경로 환경변수 설정 + export member_image_path=${props.registry}/${props.image_org}/member:${imageTag} + export store_image_path=${props.registry}/${props.image_org}/store:${imageTag} + export marketing_content_image_path=${props.registry}/${props.image_org}/marketing-content:${imageTag} + export ai_recommend_image_path=${props.registry}/${props.image_org}/ai-recommend:${imageTag} + + # manifest 생성 + envsubst < deployment/${manifest}.template > deployment/${manifest} + + echo "=== Generated Manifest File ===" + cat deployment/${manifest} + echo "===============================" + """ + } + + container('azure-cli') { + sh """ + kubectl apply -f deployment/${manifest} + + echo "Waiting for deployments to be ready..." + kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=300s + """ + } + } + } +} From 76df2807fa0ee6f4a9b26b78fec83feee9028eea Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Mon, 16 Jun 2025 16:28:55 +0900 Subject: [PATCH 12/33] =?UTF-8?q?refactor:=20acr=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smarketing-ai/deployment/manifest/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-ai/deployment/manifest/deployment.yaml b/smarketing-ai/deployment/manifest/deployment.yaml index 1a5df1d..cc53cb5 100644 --- a/smarketing-ai/deployment/manifest/deployment.yaml +++ b/smarketing-ai/deployment/manifest/deployment.yaml @@ -19,7 +19,7 @@ spec: - name: acr-secret containers: - name: smarketing - image: dg0408cr.azurecr.io/smarketing-ai:latest + image: acrdigitalgarage02.azurecr.io/smarketing-ai:latest imagePullPolicy: Always ports: - containerPort: 5001 From 3e09e777079ca8f8008fe8ccd0e00f0458911254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:38:00 +0900 Subject: [PATCH 13/33] fix blog post --- smarketing-ai/services/sns_content_service.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index c8605c8..680a1e7 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1774,12 +1774,6 @@ class SnsContentService: - 필수 키워드: {', '.join(seo_keywords[:8])} - 카테고리 키워드: {', '.join(category_keywords[:5])} -**📖 블로그 포스트 구조 (이미지 배치 포함):** -1. **인트로**: 방문 동기와 첫인상 + [IMAGE_1] 배치 -2. **매장 정보**: 위치, 운영시간, 분위기 + [IMAGE_2, IMAGE_3] 배치 -3. **메뉴 소개**: 주문한 메뉴와 상세 후기 + [IMAGE_4, IMAGE_5] 배치 -4. **총평**: 재방문 의향과 추천 이유 + [IMAGE_6] 배치 - **💡 콘텐츠 작성 지침:** 1. 검색자의 궁금증을 해결하는 정보 중심 작성 2. 구체적인 가격, 위치, 운영시간 등 실용 정보 포함 @@ -1886,12 +1880,28 @@ class SnsContentService: # 4. 추가 정리: \r, 여러 공백 정리 content = content.replace('\\r', '').replace('\r', '') - # 5. 여러 개의
태그를 하나로 정리 + # 6. 여러 개의
태그를 하나로 정리 import re content = re.sub(r'(
\s*){3,}', '

', content) - # 6. 해시태그를 파란색으로 스타일링 - content = re.sub(r'(#[\w가-힣]+)', r'\1', content) + # 7. ⭐ 간단한 해시태그 스타일링 (CSS 충돌 방지) + import re + # style="..." 패턴을 먼저 찾아서 보호 + style_patterns = re.findall(r'style="[^"]*"', content) + protected_content = content + + for i, pattern in enumerate(style_patterns): + protected_content = protected_content.replace(pattern, f'___STYLE_{i}___') + + # 이제 안전하게 해시태그 스타일링 + protected_content = re.sub(r'(#[\w가-힣]+)', r'\1', + protected_content) + + # 보호된 스타일 복원 + for i, pattern in enumerate(style_patterns): + protected_content = protected_content.replace(f'___STYLE_{i}___', pattern) + + content = protected_content # 플랫폼별 헤더 스타일 platform_style = "" From c5c950fcb75186e079e4ca8cbb2d481d2236e0a1 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 16:44:22 +0900 Subject: [PATCH 14/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 43 +------------------------- 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 1b0910f..f8a2f95 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -14,12 +14,7 @@ podTemplate( containerTemplate(name: 'gradle', image: 'gradle:jdk17', ttyEnabled: true, - command: 'cat', - envVars: [ - envVar(key: 'DOCKER_HOST', value: 'unix:///run/podman/podman.sock'), - envVar(key: 'TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE', value: '/run/podman/podman.sock'), - envVar(key: 'TESTCONTAINERS_RYUK_DISABLED', value: 'true') - ]), + command: 'cat'), containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true), containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h') ], @@ -42,42 +37,6 @@ podTemplate( namespace = "${props.namespace}" } - stage('Code Analysis & Quality Gate') { - container('gradle') { - sh "./gradlew clean build -x test" - - // SonarQube 분석 - withSonarQubeEnv('SonarQube Server') { - sh """ - ./gradlew sonarqube \\ - -Dsonar.projectKey=smarketing-backend-member-kros235 \\ - -Dsonar.host.url=\$SONAR_HOST_URL \\ - -Dsonar.login=\$SONAR_AUTH_TOKEN - - ./gradlew sonarqube \\ - -Dsonar.projectKey=smarketing-backend-store-kros235 \\ - -Dsonar.host.url=\$SONAR_HOST_URL \\ - -Dsonar.login=\$SONAR_AUTH_TOKEN - - ./gradlew sonarqube \\ - -Dsonar.projectKey=smarketing-backend-marketing-content-kros235 \\ - -Dsonar.host.url=\$SONAR_HOST_URL \\ - -Dsonar.login=\$SONAR_AUTH_TOKEN - - ./gradlew sonarqube \\ - -Dsonar.projectKey=smarketing-backend-ai-recommend-kros235 \\ - -Dsonar.host.url=\$SONAR_HOST_URL \\ - -Dsonar.login=\$SONAR_AUTH_TOKEN - """ - } - - // Quality Gate 확인 - timeout(time: 5, unit: 'MINUTES') { - waitForQualityGate abortPipeline: true - } - } - } - stage('Build Application') { container('gradle') { sh """ From 36ee23927d17bbd64b5edecc57730fd7249157b8 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:03:23 +0900 Subject: [PATCH 15/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index f8a2f95..af01384 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -40,6 +40,7 @@ podTemplate( stage('Build Application') { container('gradle') { sh """ + chmod +x gradlew ./gradlew :member:clean :member:build -x test ./gradlew :store:clean :store:build -x test ./gradlew :marketing-content:clean :marketing-content:build -x test @@ -50,18 +51,23 @@ podTemplate( stage('Build & Push Images') { container('podman') { - services.each { service -> - script { - // mysub 서비스 특별 처리 (요구사항에 없지만 참고용 주석) - // def buildDir = service == 'mysub' ? 'mysub-infra' : service + withCredentials([usernamePassword( + credentialsId: 'acr-credentials', + usernameVariable: 'USERNAME', + passwordVariable: 'PASSWORD' + )]) { + sh "podman login ${props.registry} --username \$USERNAME --password \$PASSWORD" + + services.each { service -> + def buildDir = service def jarFile = "${service}.jar" sh """ - podman build \\ - --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ - --build-arg ARTIFACTORY_FILE="${jarFile}" \\ - -f deployment/container/Dockerfile \\ + podman build \ + --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \ + --build-arg ARTIFACTORY_FILE="${jarFile}" \ + -f deployment/container/Dockerfile \ -t ${props.registry}/${props.image_org}/${service}:${imageTag} . podman push ${props.registry}/${props.image_org}/${service}:${imageTag} From 68baed5d27504cdadcfce26fe0378c7a5a1a103b Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:09:56 +0900 Subject: [PATCH 16/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index af01384..b9a7857 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -33,7 +33,7 @@ podTemplate( stage("Get Source") { checkout scm - props = readProperties file: "deployment/deploy_env_vars" + props = readProperties file: "smarketing-java/deployment/deploy_env_vars" namespace = "${props.namespace}" } @@ -67,7 +67,7 @@ podTemplate( podman build \ --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \ --build-arg ARTIFACTORY_FILE="${jarFile}" \ - -f deployment/container/Dockerfile \ + -f smarketing-java/deployment/container/Dockerfile \ -t ${props.registry}/${props.image_org}/${service}:${imageTag} . podman push ${props.registry}/${props.image_org}/${service}:${imageTag} @@ -98,23 +98,23 @@ podTemplate( export ai_recommend_image_path=${props.registry}/${props.image_org}/ai-recommend:${imageTag} # manifest 생성 - envsubst < deployment/${manifest}.template > deployment/${manifest} + envsubst < smarketing-java/deployment/${manifest}.template > smarketing-java/deployment/${manifest} echo "=== Generated Manifest File ===" - cat deployment/${manifest} + cat smarketing-java/deployment/${manifest} echo "===============================" """ } container('azure-cli') { sh """ - kubectl apply -f deployment/${manifest} + kubectl apply -f smarketing-java/deployment/${manifest} echo "Waiting for deployments to be ready..." - kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=300s - kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=300s - kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=300s - kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=300s + kubectl -n ${namespace} wait --for=condition=available smarketing-java/deployment/member --timeout=300s + kubectl -n ${namespace} wait --for=condition=available smarketing-java/deployment/store --timeout=300s + kubectl -n ${namespace} wait --for=condition=available smarketing-java/deployment/marketing-content --timeout=300s + kubectl -n ${namespace} wait --for=condition=available smarketing-java/deployment/ai-recommend --timeout=300s """ } } From ad0d6bb19757e589b160ab3e9af2a5c95e8488ea Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:12:53 +0900 Subject: [PATCH 17/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index b9a7857..3126a46 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -37,6 +37,30 @@ podTemplate( namespace = "${props.namespace}" } + stage('Debug Project Structure') { + container('gradle') { + sh """ + echo "=== 현재 디렉토리 확인 ===" + pwd + + echo "=== 루트 디렉토리 파일 목록 ===" + ls -la + + echo "=== gradlew 파일 찾기 ===" + find . -name "gradlew" -type f 2>/dev/null || echo "gradlew 파일을 찾을 수 없습니다" + + echo "=== build.gradle 파일 찾기 ===" + find . -name "build.gradle" -type f 2>/dev/null || echo "build.gradle 파일을 찾을 수 없습니다" + + echo "=== settings.gradle 파일 찾기 ===" + find . -name "settings.gradle" -type f 2>/dev/null || echo "settings.gradle 파일을 찾을 수 없습니다" + + echo "=== 디렉토리 트리 구조 (깊이 2) ===" + find . -maxdepth 2 -type d | sort + """ + } + } + stage('Build Application') { container('gradle') { sh """ From 610915add98036e82675ab692ed142c2824f8280 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:16:49 +0900 Subject: [PATCH 18/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 28 ++++---------------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 3126a46..021d0da 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -37,34 +37,14 @@ podTemplate( namespace = "${props.namespace}" } - stage('Debug Project Structure') { - container('gradle') { - sh """ - echo "=== 현재 디렉토리 확인 ===" - pwd - - echo "=== 루트 디렉토리 파일 목록 ===" - ls -la - - echo "=== gradlew 파일 찾기 ===" - find . -name "gradlew" -type f 2>/dev/null || echo "gradlew 파일을 찾을 수 없습니다" - - echo "=== build.gradle 파일 찾기 ===" - find . -name "build.gradle" -type f 2>/dev/null || echo "build.gradle 파일을 찾을 수 없습니다" - - echo "=== settings.gradle 파일 찾기 ===" - find . -name "settings.gradle" -type f 2>/dev/null || echo "settings.gradle 파일을 찾을 수 없습니다" - - echo "=== 디렉토리 트리 구조 (깊이 2) ===" - find . -maxdepth 2 -type d | sort - """ - } - } stage('Build Application') { container('gradle') { sh """ - chmod +x gradlew + cd smarketing-java + + chmod +x gradlew + ./gradlew :member:clean :member:build -x test ./gradlew :store:clean :store:build -x test ./gradlew :marketing-content:clean :marketing-content:build -x test From 845f3fdd039894195b7843bccc976cd034ff21b1 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:20:52 +0900 Subject: [PATCH 19/33] Update deploy_env_vars --- smarketing-java/deployment/deploy_env_vars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-java/deployment/deploy_env_vars b/smarketing-java/deployment/deploy_env_vars index fe5de69..9528fb9 100644 --- a/smarketing-java/deployment/deploy_env_vars +++ b/smarketing-java/deployment/deploy_env_vars @@ -4,7 +4,7 @@ root_project=smarketing-backend namespace=smarketing # Container Registry Settings -registry=unicorncr.azurecr.io +registry=acrdigitalgarage02.azurecr.io image_org=smarketing # Application Settings From 0d6f435f68112a512cdae7149eb0f1ecd5712a99 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:29:13 +0900 Subject: [PATCH 20/33] Create Dockerfile --- .../deployment/container/Dockerfile | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 smarketing-java/deployment/container/Dockerfile diff --git a/smarketing-java/deployment/container/Dockerfile b/smarketing-java/deployment/container/Dockerfile new file mode 100644 index 0000000..f0717f1 --- /dev/null +++ b/smarketing-java/deployment/container/Dockerfile @@ -0,0 +1,46 @@ +# Multi-stage build for Spring Boot application +FROM gradle:8.13-jdk17 AS builder + +# Build arguments +ARG BUILD_LIB_DIR +ARG ARTIFACTORY_FILE + +WORKDIR /app + +# Copy source code (assumed to be already built) +# The JAR file should be copied from the build context +COPY ${BUILD_LIB_DIR}/${ARTIFACTORY_FILE} app.jar + +# Runtime stage +FROM openjdk:17-jre-slim + +# Install necessary packages +RUN apt-get update && apt-get install -y \ + curl \ + netcat-traditional \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Set working directory +WORKDIR /app + +# Copy JAR from builder stage +COPY --from=builder /app/app.jar app.jar + +# Change ownership +RUN chown -R appuser:appuser /app + +# Switch to non-root user +USER appuser + +# Expose port (will be overridden by environment variables) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/actuator/health || exit 1 + +# Run the application +ENTRYPOINT ["java", "-jar", "app.jar"] From a7a046bf43405b774a1cafce8322d195dccb3815 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:37:45 +0900 Subject: [PATCH 21/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 56 +++++++++++++++++++------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 021d0da..11e197d 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -37,7 +37,6 @@ podTemplate( namespace = "${props.namespace}" } - stage('Build Application') { container('gradle') { sh """ @@ -49,6 +48,9 @@ podTemplate( ./gradlew :store:clean :store:build -x test ./gradlew :marketing-content:clean :marketing-content:build -x test ./gradlew :ai-recommend:clean :ai-recommend:build -x test + + echo "=== 빌드 결과 확인 ===" + find . -name "*.jar" -path "*/build/libs/*" """ } } @@ -63,19 +65,43 @@ podTemplate( sh "podman login ${props.registry} --username \$USERNAME --password \$PASSWORD" services.each { service -> + script { + // 🔧 핵심 수정: smarketing-java 경로 포함 + def buildDir = "smarketing-java/${service}" + def jarFile = "${service}.jar" + def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}" - def buildDir = service - def jarFile = "${service}.jar" + echo "Building image for ${service}: ${fullImageName}" + echo "JAR 파일 경로: ${buildDir}/build/libs/${jarFile}" + + // JAR 파일 존재 확인 + sh """ + echo "=== ${service} JAR 파일 확인 ===" + if [ -f "${buildDir}/build/libs/${jarFile}" ]; then + echo "JAR 파일 발견: ${buildDir}/build/libs/${jarFile}" + ls -la ${buildDir}/build/libs/${jarFile} + else + echo "오류: JAR 파일을 찾을 수 없습니다: ${buildDir}/build/libs/${jarFile}" + echo "실제 존재하는 파일들:" + find . -name "*.jar" -path "*${service}*" || echo "JAR 파일이 없습니다" + exit 1 + fi + """ - sh """ - podman build \ - --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \ - --build-arg ARTIFACTORY_FILE="${jarFile}" \ - -f smarketing-java/deployment/container/Dockerfile \ - -t ${props.registry}/${props.image_org}/${service}:${imageTag} . + sh """ + echo "=== ${service} 이미지 빌드 ===" + podman build \\ + --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ + --build-arg ARTIFACTORY_FILE="${jarFile}" \\ + -f smarketing-java/deployment/container/Dockerfile \\ + -t ${fullImageName} . - podman push ${props.registry}/${props.image_org}/${service}:${imageTag} - """ + echo "=== ${service} 이미지 푸시 ===" + podman push ${fullImageName} + + echo "Successfully built and pushed: ${fullImageName}" + """ + } } } } @@ -115,10 +141,10 @@ podTemplate( kubectl apply -f smarketing-java/deployment/${manifest} echo "Waiting for deployments to be ready..." - kubectl -n ${namespace} wait --for=condition=available smarketing-java/deployment/member --timeout=300s - kubectl -n ${namespace} wait --for=condition=available smarketing-java/deployment/store --timeout=300s - kubectl -n ${namespace} wait --for=condition=available smarketing-java/deployment/marketing-content --timeout=300s - kubectl -n ${namespace} wait --for=condition=available smarketing-java/deployment/ai-recommend --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=300s """ } } From 79b481585baad0fcc029a525342dfc6ea8332d20 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:42:16 +0900 Subject: [PATCH 22/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 33 +++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 11e197d..350faa6 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -66,33 +66,32 @@ podTemplate( services.each { service -> script { - // 🔧 핵심 수정: smarketing-java 경로 포함 def buildDir = "smarketing-java/${service}" - def jarFile = "${service}.jar" def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}" echo "Building image for ${service}: ${fullImageName}" - echo "JAR 파일 경로: ${buildDir}/build/libs/${jarFile}" - // JAR 파일 존재 확인 - sh """ - echo "=== ${service} JAR 파일 확인 ===" - if [ -f "${buildDir}/build/libs/${jarFile}" ]; then - echo "JAR 파일 발견: ${buildDir}/build/libs/${jarFile}" - ls -la ${buildDir}/build/libs/${jarFile} - else - echo "오류: JAR 파일을 찾을 수 없습니다: ${buildDir}/build/libs/${jarFile}" - echo "실제 존재하는 파일들:" - find . -name "*.jar" -path "*${service}*" || echo "JAR 파일이 없습니다" - exit 1 - fi - """ + // 🔧 실제 JAR 파일명 동적 탐지 + def actualJarFile = sh( + script: """ + cd ${buildDir}/build/libs + # -plain.jar 파일 제외하고 메인 JAR 파일 찾기 + ls *.jar | grep -v 'plain.jar' | head -1 + """, + returnStdout: true + ).trim() + + if (!actualJarFile) { + error "${service} JAR 파일을 찾을 수 없습니다" + } + + echo "발견된 JAR 파일: ${actualJarFile}" sh """ echo "=== ${service} 이미지 빌드 ===" podman build \\ --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ - --build-arg ARTIFACTORY_FILE="${jarFile}" \\ + --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\ -f smarketing-java/deployment/container/Dockerfile \\ -t ${fullImageName} . From d6c0b04d31cbd9aff812a7ae124d367f0f7aff2c Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:47:11 +0900 Subject: [PATCH 23/33] Update Dockerfile --- smarketing-java/deployment/container/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-java/deployment/container/Dockerfile b/smarketing-java/deployment/container/Dockerfile index f0717f1..f4ec12d 100644 --- a/smarketing-java/deployment/container/Dockerfile +++ b/smarketing-java/deployment/container/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /app COPY ${BUILD_LIB_DIR}/${ARTIFACTORY_FILE} app.jar # Runtime stage -FROM openjdk:17-jre-slim +FROM eclipse-temurin:17-jre # Install necessary packages RUN apt-get update && apt-get install -y \ From 2d17e43ad281b09c168d9bfdc467f3785312896d Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 17:55:37 +0900 Subject: [PATCH 24/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 41 ++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 350faa6..52bb7e2 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -35,6 +35,33 @@ podTemplate( checkout scm props = readProperties file: "smarketing-java/deployment/deploy_env_vars" namespace = "${props.namespace}" + + echo "네임스페이스: ${namespace}" + echo "팀 ID: ${props.teamid}" + } + + // 🔧 핵심 추가: AKS 설정 단계 + stage("Setup AKS") { + container('azure-cli') { + withCredentials([azureServicePrincipal('azure-credentials')]) { + sh """ + echo "=== Azure 로그인 ===" + az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID + + echo "=== AKS 인증정보 가져오기 ===" + az aks get-credentials --resource-group rg-digitalgarage-01 --name aks-digitalgarage-01 --overwrite-existing + + echo "=== kubectl 연결 테스트 ===" + kubectl cluster-info + + echo "=== 네임스페이스 생성 ===" + kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f - + + echo "=== 현재 컨텍스트 확인 ===" + kubectl config current-context + """ + } + } } stage('Build Application') { @@ -71,11 +98,10 @@ podTemplate( echo "Building image for ${service}: ${fullImageName}" - // 🔧 실제 JAR 파일명 동적 탐지 + // 실제 JAR 파일명 동적 탐지 def actualJarFile = sh( script: """ cd ${buildDir}/build/libs - # -plain.jar 파일 제외하고 메인 JAR 파일 찾기 ls *.jar | grep -v 'plain.jar' | head -1 """, returnStdout: true @@ -137,13 +163,22 @@ podTemplate( container('azure-cli') { sh """ + echo "=== kubectl 연결 상태 재확인 ===" + kubectl cluster-info + + echo "=== Manifest 적용 ===" kubectl apply -f smarketing-java/deployment/${manifest} - echo "Waiting for deployments to be ready..." + echo "=== Deployment 대기 ===" kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=300s kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=300s kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=300s kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=300s + + echo "=== 배포 상태 확인 ===" + kubectl -n ${namespace} get deployments + kubectl -n ${namespace} get pods + kubectl -n ${namespace} get services """ } } From 1c461f65f43d8766b8a555eda5ad7a595b41a8f5 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Mon, 16 Jun 2025 18:08:52 +0900 Subject: [PATCH 25/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 49 +++++++++++++++++++------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 52bb7e2..d6c554c 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -40,7 +40,6 @@ podTemplate( echo "팀 ID: ${props.teamid}" } - // 🔧 핵심 추가: AKS 설정 단계 stage("Setup AKS") { container('azure-cli') { withCredentials([azureServicePrincipal('azure-credentials')]) { @@ -57,8 +56,13 @@ podTemplate( echo "=== 네임스페이스 생성 ===" kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f - - echo "=== 현재 컨텍스트 확인 ===" - kubectl config current-context + echo "=== Image Pull Secret 생성 ===" + kubectl create secret docker-registry acr-secret \\ + --docker-server=${props.registry} \\ + --docker-username=acrdigitalgarage02 \\ + --docker-password=\$(az acr credential show --name acrdigitalgarage02 --query passwords[0].value -o tsv) \\ + --namespace=${namespace} \\ + --dry-run=client -o yaml | kubectl apply -f - """ } } @@ -163,22 +167,43 @@ podTemplate( container('azure-cli') { sh """ - echo "=== kubectl 연결 상태 재확인 ===" - kubectl cluster-info - echo "=== Manifest 적용 ===" kubectl apply -f smarketing-java/deployment/${manifest} - echo "=== Deployment 대기 ===" - kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=300s - kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=300s - kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=300s - kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=300s + echo "=== 배포 상태 즉시 확인 ===" + kubectl -n ${namespace} get deployments + kubectl -n ${namespace} get pods + kubectl -n ${namespace} get events --sort-by='.lastTimestamp' | tail -20 - echo "=== 배포 상태 확인 ===" + echo "=== Pod 상세 정보 확인 ===" + for pod in \$(kubectl -n ${namespace} get pods -o name); do + echo "=== \$pod 상세 정보 ===" + kubectl -n ${namespace} describe \$pod + echo "=== \$pod 로그 ===" + kubectl -n ${namespace} logs \$pod --tail=50 || echo "로그 없음" + echo "================" + done + + echo "=== Deployment 대기 (30초 timeout으로 단축) ===" + timeout 30 kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=30s || echo "member deployment 대기 실패" + timeout 30 kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=30s || echo "store deployment 대기 실패" + timeout 30 kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=30s || echo "marketing-content deployment 대기 실패" + timeout 30 kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=30s || echo "ai-recommend deployment 대기 실패" + + echo "=== 최종 배포 상태 확인 ===" kubectl -n ${namespace} get deployments kubectl -n ${namespace} get pods kubectl -n ${namespace} get services + + echo "=== 실패한 Pod가 있다면 상세 로그 ===" + for pod in \$(kubectl -n ${namespace} get pods --field-selector=status.phase!=Running -o name 2>/dev/null || true); do + if [ ! -z "\$pod" ]; then + echo "=== 실패한 Pod: \$pod ===" + kubectl -n ${namespace} describe \$pod + kubectl -n ${namespace} logs \$pod --previous --tail=50 2>/dev/null || echo "이전 로그 없음" + kubectl -n ${namespace} logs \$pod --tail=50 2>/dev/null || echo "현재 로그 없음" + fi + done """ } } From 42c071fbf04563b47e9b394b4c5e0d657e703f87 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 09:36:09 +0900 Subject: [PATCH 26/33] Update Dockerfile --- .../deployment/container/Dockerfile | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/smarketing-java/deployment/container/Dockerfile b/smarketing-java/deployment/container/Dockerfile index f4ec12d..be0f578 100644 --- a/smarketing-java/deployment/container/Dockerfile +++ b/smarketing-java/deployment/container/Dockerfile @@ -1,17 +1,11 @@ -# Multi-stage build for Spring Boot application -FROM gradle:8.13-jdk17 AS builder - -# Build arguments +# Build stage +FROM eclipse-temurin:17-jre AS builder ARG BUILD_LIB_DIR ARG ARTIFACTORY_FILE - WORKDIR /app - -# Copy source code (assumed to be already built) -# The JAR file should be copied from the build context COPY ${BUILD_LIB_DIR}/${ARTIFACTORY_FILE} app.jar -# Runtime stage +# Run stage FROM eclipse-temurin:17-jre # Install necessary packages @@ -20,22 +14,25 @@ RUN apt-get update && apt-get install -y \ netcat-traditional \ && rm -rf /var/lib/apt/lists/* -# Create non-root user -RUN groupadd -r appuser && useradd -r -g appuser appuser +ENV USERNAME k8s +ENV ARTIFACTORY_HOME /home/${USERNAME} +ENV JAVA_OPTS="" -# Set working directory -WORKDIR /app +# Add a non-root user +RUN groupadd -r ${USERNAME} && useradd -r -g ${USERNAME} ${USERNAME} && \ + mkdir -p ${ARTIFACTORY_HOME} && \ + chown ${USERNAME}:${USERNAME} ${ARTIFACTORY_HOME} + +WORKDIR ${ARTIFACTORY_HOME} # Copy JAR from builder stage COPY --from=builder /app/app.jar app.jar - -# Change ownership -RUN chown -R appuser:appuser /app +RUN chown ${USERNAME}:${USERNAME} app.jar # Switch to non-root user -USER appuser +USER ${USERNAME} -# Expose port (will be overridden by environment variables) +# Expose port EXPOSE 8080 # Health check @@ -43,4 +40,5 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 # Run the application -ENTRYPOINT ["java", "-jar", "app.jar"] +ENTRYPOINT ["sh", "-c"] +CMD ["java ${JAVA_OPTS} -jar app.jar"] From d244f040ad242878ea24b71139bae0a0138a860e Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 09:36:26 +0900 Subject: [PATCH 27/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 105 +++++++++++++------------ 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index d6c554c..e0b27d4 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -10,18 +10,15 @@ podTemplate( label: "${PIPELINE_ID}", serviceAccount: 'jenkins', containers: [ - containerTemplate(name: 'podman', image: "mgoltzsche/podman", ttyEnabled: true, command: 'cat', privileged: true), - containerTemplate(name: 'gradle', - image: 'gradle:jdk17', - ttyEnabled: true, - command: 'cat'), + containerTemplate(name: 'gradle', image: 'gradle:jdk17', ttyEnabled: true, command: 'cat'), + containerTemplate(name: 'docker', image: 'docker:20.10.16-dind', ttyEnabled: true, privileged: true), containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true), containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h') ], volumes: [ emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false), emptyDirVolume(mountPath: '/root/.azure', memory: false), - emptyDirVolume(mountPath: '/run/podman', memory: false) + emptyDirVolume(mountPath: '/var/run', memory: false) ] ) { node(PIPELINE_ID) { @@ -33,11 +30,15 @@ podTemplate( stage("Get Source") { checkout scm + + // smarketing-java 하위에 있는 설정 파일 읽기 props = readProperties file: "smarketing-java/deployment/deploy_env_vars" namespace = "${props.namespace}" - - echo "네임스페이스: ${namespace}" - echo "팀 ID: ${props.teamid}" + + echo "=== Build Information ===" + echo "Services: ${services}" + echo "Namespace: ${namespace}" + echo "Image Tag: ${imageTag}" } stage("Setup AKS") { @@ -46,13 +47,11 @@ podTemplate( sh """ echo "=== Azure 로그인 ===" az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID + az account set --subscription 2513dd36-7978-48e3-9a7c-b221d4874f66 echo "=== AKS 인증정보 가져오기 ===" az aks get-credentials --resource-group rg-digitalgarage-01 --name aks-digitalgarage-01 --overwrite-existing - echo "=== kubectl 연결 테스트 ===" - kubectl cluster-info - echo "=== 네임스페이스 생성 ===" kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f - @@ -63,37 +62,48 @@ podTemplate( --docker-password=\$(az acr credential show --name acrdigitalgarage02 --query passwords[0].value -o tsv) \\ --namespace=${namespace} \\ --dry-run=client -o yaml | kubectl apply -f - + + echo "=== 클러스터 상태 확인 ===" + kubectl get nodes + kubectl get ns ${namespace} """ } } } - stage('Build Application') { + stage('Build Applications') { container('gradle') { sh """ + echo "=== smarketing-java 디렉토리로 이동 ===" cd smarketing-java + echo "=== gradlew 권한 설정 ===" chmod +x gradlew + echo "=== 전체 서비스 빌드 ===" ./gradlew :member:clean :member:build -x test ./gradlew :store:clean :store:build -x test ./gradlew :marketing-content:clean :marketing-content:build -x test ./gradlew :ai-recommend:clean :ai-recommend:build -x test echo "=== 빌드 결과 확인 ===" - find . -name "*.jar" -path "*/build/libs/*" + find . -name "*.jar" -path "*/build/libs/*" | grep -v 'plain.jar' """ } } stage('Build & Push Images') { - container('podman') { - withCredentials([usernamePassword( - credentialsId: 'acr-credentials', - usernameVariable: 'USERNAME', - passwordVariable: 'PASSWORD' - )]) { - sh "podman login ${props.registry} --username \$USERNAME --password \$PASSWORD" + container('docker') { + withCredentials([azureServicePrincipal('azure-credentials')]) { + sh """ + echo "=== Docker 데몬 시작 대기 ===" + timeout 30 sh -c 'until docker info; do sleep 1; done' + + echo "=== Azure 로그인 및 ACR 로그인 ===" + az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID + az account set --subscription 2513dd36-7978-48e3-9a7c-b221d4874f66 + az acr login --name acrdigitalgarage02 + """ services.each { service -> script { @@ -119,14 +129,14 @@ podTemplate( sh """ echo "=== ${service} 이미지 빌드 ===" - podman build \\ + docker build \\ --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\ -f smarketing-java/deployment/container/Dockerfile \\ -t ${fullImageName} . echo "=== ${service} 이미지 푸시 ===" - podman push ${fullImageName} + docker push ${fullImageName} echo "Successfully built and pushed: ${fullImageName}" """ @@ -139,16 +149,18 @@ podTemplate( stage('Generate & Apply Manifest') { container('envsubst') { sh """ + echo "=== 환경변수 설정 ===" export namespace=${namespace} export allowed_origins=${props.allowed_origins} export jwt_secret_key=${props.jwt_secret_key} export postgres_user=${props.postgres_user} export postgres_password=${props.postgres_password} export replicas=${props.replicas} - export resources_requests_cpu=${props.resources_requests_cpu} - export resources_requests_memory=${props.resources_requests_memory} - export resources_limits_cpu=${props.resources_limits_cpu} - export resources_limits_memory=${props.resources_limits_memory} + # 리소스 요구사항 조정 (작게) + export resources_requests_cpu=100m + export resources_requests_memory=128Mi + export resources_limits_cpu=500m + export resources_limits_memory=512Mi # 이미지 경로 환경변수 설정 export member_image_path=${props.registry}/${props.image_org}/member:${imageTag} @@ -156,7 +168,7 @@ podTemplate( export marketing_content_image_path=${props.registry}/${props.image_org}/marketing-content:${imageTag} export ai_recommend_image_path=${props.registry}/${props.image_org}/ai-recommend:${imageTag} - # manifest 생성 + echo "=== Manifest 생성 ===" envsubst < smarketing-java/deployment/${manifest}.template > smarketing-java/deployment/${manifest} echo "=== Generated Manifest File ===" @@ -167,41 +179,30 @@ podTemplate( container('azure-cli') { sh """ + echo "=== PostgreSQL 서비스 확인 ===" + kubectl get svc -n ${namespace} | grep postgresql || echo "PostgreSQL 서비스가 없습니다. 먼저 설치해주세요." + echo "=== Manifest 적용 ===" kubectl apply -f smarketing-java/deployment/${manifest} - echo "=== 배포 상태 즉시 확인 ===" + echo "=== 배포 상태 확인 (60초 대기) ===" kubectl -n ${namespace} get deployments kubectl -n ${namespace} get pods - kubectl -n ${namespace} get events --sort-by='.lastTimestamp' | tail -20 - echo "=== Pod 상세 정보 확인 ===" - for pod in \$(kubectl -n ${namespace} get pods -o name); do - echo "=== \$pod 상세 정보 ===" - kubectl -n ${namespace} describe \$pod - echo "=== \$pod 로그 ===" - kubectl -n ${namespace} logs \$pod --tail=50 || echo "로그 없음" - echo "================" - done - - echo "=== Deployment 대기 (30초 timeout으로 단축) ===" - timeout 30 kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=30s || echo "member deployment 대기 실패" - timeout 30 kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=30s || echo "store deployment 대기 실패" - timeout 30 kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=30s || echo "marketing-content deployment 대기 실패" - timeout 30 kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=30s || echo "ai-recommend deployment 대기 실패" + echo "=== 각 서비스 배포 대기 (60초 timeout) ===" + timeout 60 kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=60s || echo "member deployment 대기 타임아웃" + timeout 60 kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=60s || echo "store deployment 대기 타임아웃" + timeout 60 kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=60s || echo "marketing-content deployment 대기 타임아웃" + timeout 60 kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=60s || echo "ai-recommend deployment 대기 타임아웃" - echo "=== 최종 배포 상태 확인 ===" - kubectl -n ${namespace} get deployments - kubectl -n ${namespace} get pods - kubectl -n ${namespace} get services + echo "=== 최종 상태 ===" + kubectl -n ${namespace} get all - echo "=== 실패한 Pod가 있다면 상세 로그 ===" + echo "=== 실패한 Pod 상세 정보 ===" for pod in \$(kubectl -n ${namespace} get pods --field-selector=status.phase!=Running -o name 2>/dev/null || true); do if [ ! -z "\$pod" ]; then echo "=== 실패한 Pod: \$pod ===" - kubectl -n ${namespace} describe \$pod - kubectl -n ${namespace} logs \$pod --previous --tail=50 2>/dev/null || echo "이전 로그 없음" - kubectl -n ${namespace} logs \$pod --tail=50 2>/dev/null || echo "현재 로그 없음" + kubectl -n ${namespace} describe \$pod | tail -20 fi done """ From c7288ba805f623c5eb910823105d546fa696d91d Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 09:36:38 +0900 Subject: [PATCH 28/33] Update deploy_env_vars --- smarketing-java/deployment/deploy_env_vars | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/smarketing-java/deployment/deploy_env_vars b/smarketing-java/deployment/deploy_env_vars index 9528fb9..db95eda 100644 --- a/smarketing-java/deployment/deploy_env_vars +++ b/smarketing-java/deployment/deploy_env_vars @@ -16,8 +16,8 @@ jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUe postgres_user=admin postgres_password=Hi5Jessica! -# Resource Settings -resources_requests_cpu=256m -resources_requests_memory=256Mi -resources_limits_cpu=1024m -resources_limits_memory=1024Mi +# Resource Settings (리소스 요구사항 줄임) +resources_requests_cpu=100m +resources_requests_memory=128Mi +resources_limits_cpu=500m +resources_limits_memory=512Mi From b4f16e43500ee8b5026ec69589eaa39daf74b355 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 09:41:04 +0900 Subject: [PATCH 29/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 102 +++++++++++++++---------- 1 file changed, 62 insertions(+), 40 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index e0b27d4..f2f7ebf 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -92,55 +92,77 @@ podTemplate( } } - stage('Build & Push Images') { - container('docker') { + stage('ACR Login') { + container('azure-cli') { withCredentials([azureServicePrincipal('azure-credentials')]) { sh """ - echo "=== Docker 데몬 시작 대기 ===" - timeout 30 sh -c 'until docker info; do sleep 1; done' - - echo "=== Azure 로그인 및 ACR 로그인 ===" + echo "=== Azure 로그인 ===" az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID az account set --subscription 2513dd36-7978-48e3-9a7c-b221d4874f66 - az acr login --name acrdigitalgarage02 + + echo "=== ACR 인증 토큰 생성 ===" + ACR_TOKEN=\$(az acr login --name acrdigitalgarage02 --expose-token --output tsv --query accessToken) + echo "ACR_TOKEN=\$ACR_TOKEN" > /tmp/acr_token.env + + echo "=== ACR 인증 정보 저장 ===" + echo "acrdigitalgarage02.azurecr.io" > /tmp/acr_server.txt + echo "00000000-0000-0000-0000-000000000000" > /tmp/acr_username.txt + echo "\$ACR_TOKEN" > /tmp/acr_password.txt """ + } + } + } - services.each { service -> - script { - def buildDir = "smarketing-java/${service}" - def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}" + stage('Build & Push Images') { + container('docker') { + sh """ + echo "=== Docker 데몬 시작 대기 ===" + timeout 30 sh -c 'until docker info; do sleep 1; done' + + echo "=== ACR 로그인 정보 읽기 ===" + ACR_SERVER=\$(cat /tmp/acr_server.txt) + ACR_USERNAME=\$(cat /tmp/acr_username.txt) + ACR_PASSWORD=\$(cat /tmp/acr_password.txt) + + echo "=== Docker로 ACR 로그인 ===" + echo "\$ACR_PASSWORD" | docker login \$ACR_SERVER --username \$ACR_USERNAME --password-stdin + """ - echo "Building image for ${service}: ${fullImageName}" - - // 실제 JAR 파일명 동적 탐지 - def actualJarFile = sh( - script: """ - cd ${buildDir}/build/libs - ls *.jar | grep -v 'plain.jar' | head -1 - """, - returnStdout: true - ).trim() - - if (!actualJarFile) { - error "${service} JAR 파일을 찾을 수 없습니다" - } - - echo "발견된 JAR 파일: ${actualJarFile}" + services.each { service -> + script { + def buildDir = "smarketing-java/${service}" + def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}" - sh """ - echo "=== ${service} 이미지 빌드 ===" - docker build \\ - --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ - --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\ - -f smarketing-java/deployment/container/Dockerfile \\ - -t ${fullImageName} . - - echo "=== ${service} 이미지 푸시 ===" - docker push ${fullImageName} - - echo "Successfully built and pushed: ${fullImageName}" - """ + echo "Building image for ${service}: ${fullImageName}" + + // 실제 JAR 파일명 동적 탐지 + def actualJarFile = sh( + script: """ + cd ${buildDir}/build/libs + ls *.jar | grep -v 'plain.jar' | head -1 + """, + returnStdout: true + ).trim() + + if (!actualJarFile) { + error "${service} JAR 파일을 찾을 수 없습니다" } + + echo "발견된 JAR 파일: ${actualJarFile}" + + sh """ + echo "=== ${service} 이미지 빌드 ===" + docker build \\ + --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ + --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\ + -f smarketing-java/deployment/container/Dockerfile \\ + -t ${fullImageName} . + + echo "=== ${service} 이미지 푸시 ===" + docker push ${fullImageName} + + echo "Successfully built and pushed: ${fullImageName}" + """ } } } From 200b41189aa4f1a46ab16df5ce5d62fc22a4a8a2 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 09:49:44 +0900 Subject: [PATCH 30/33] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 105 +++++++++++-------------- 1 file changed, 44 insertions(+), 61 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index f2f7ebf..b281bd7 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -92,77 +92,60 @@ podTemplate( } } - stage('ACR Login') { - container('azure-cli') { - withCredentials([azureServicePrincipal('azure-credentials')]) { - sh """ - echo "=== Azure 로그인 ===" - az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID - az account set --subscription 2513dd36-7978-48e3-9a7c-b221d4874f66 - - echo "=== ACR 인증 토큰 생성 ===" - ACR_TOKEN=\$(az acr login --name acrdigitalgarage02 --expose-token --output tsv --query accessToken) - echo "ACR_TOKEN=\$ACR_TOKEN" > /tmp/acr_token.env - - echo "=== ACR 인증 정보 저장 ===" - echo "acrdigitalgarage02.azurecr.io" > /tmp/acr_server.txt - echo "00000000-0000-0000-0000-000000000000" > /tmp/acr_username.txt - echo "\$ACR_TOKEN" > /tmp/acr_password.txt - """ - } - } - } - stage('Build & Push Images') { container('docker') { sh """ echo "=== Docker 데몬 시작 대기 ===" timeout 30 sh -c 'until docker info; do sleep 1; done' - - echo "=== ACR 로그인 정보 읽기 ===" - ACR_SERVER=\$(cat /tmp/acr_server.txt) - ACR_USERNAME=\$(cat /tmp/acr_username.txt) - ACR_PASSWORD=\$(cat /tmp/acr_password.txt) - - echo "=== Docker로 ACR 로그인 ===" - echo "\$ACR_PASSWORD" | docker login \$ACR_SERVER --username \$ACR_USERNAME --password-stdin """ - services.each { service -> - script { - def buildDir = "smarketing-java/${service}" - def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}" + // 🔧 ACR Credential을 Jenkins에서 직접 사용 + withCredentials([usernamePassword( + credentialsId: 'acr-credentials', + usernameVariable: 'ACR_USERNAME', + passwordVariable: 'ACR_PASSWORD' + )]) { + sh """ + echo "=== Docker로 ACR 로그인 ===" + echo "\$ACR_PASSWORD" | docker login ${props.registry} --username \$ACR_USERNAME --password-stdin + """ - echo "Building image for ${service}: ${fullImageName}" - - // 실제 JAR 파일명 동적 탐지 - def actualJarFile = sh( - script: """ - cd ${buildDir}/build/libs - ls *.jar | grep -v 'plain.jar' | head -1 - """, - returnStdout: true - ).trim() - - if (!actualJarFile) { - error "${service} JAR 파일을 찾을 수 없습니다" - } - - echo "발견된 JAR 파일: ${actualJarFile}" + services.each { service -> + script { + def buildDir = "smarketing-java/${service}" + def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}" - sh """ - echo "=== ${service} 이미지 빌드 ===" - docker build \\ - --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ - --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\ - -f smarketing-java/deployment/container/Dockerfile \\ - -t ${fullImageName} . - - echo "=== ${service} 이미지 푸시 ===" - docker push ${fullImageName} + echo "Building image for ${service}: ${fullImageName}" - echo "Successfully built and pushed: ${fullImageName}" - """ + // 실제 JAR 파일명 동적 탐지 + def actualJarFile = sh( + script: """ + cd ${buildDir}/build/libs + ls *.jar | grep -v 'plain.jar' | head -1 + """, + returnStdout: true + ).trim() + + if (!actualJarFile) { + error "${service} JAR 파일을 찾을 수 없습니다" + } + + echo "발견된 JAR 파일: ${actualJarFile}" + + sh """ + echo "=== ${service} 이미지 빌드 ===" + docker build \\ + --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ + --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\ + -f smarketing-java/deployment/container/Dockerfile \\ + -t ${fullImageName} . + + echo "=== ${service} 이미지 푸시 ===" + docker push ${fullImageName} + + echo "Successfully built and pushed: ${fullImageName}" + """ + } } } } From faaf690db28ec9dce61fd226de56919e1c7a730b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:54:49 +0900 Subject: [PATCH 31/33] add python call --- .../marketing-content/build.gradle | 3 + .../service/ContentQueryService.java | 58 +-- .../service/PosterContentService.java | 8 +- .../service/SnsContentService.java | 47 +-- .../usecase/ContentQueryUseCase.java | 2 +- .../content/config/WebClientConfig.java | 31 ++ .../domain/model/CreationConditions.java | 7 +- .../domain/service/AiContentGenerator.java | 10 +- .../external/ClaudeAiContentGenerator.java | 144 +++---- .../infrastructure/mapper/ContentMapper.java | 8 +- .../controller/ContentController.java | 16 +- .../dto/SnsContentCreateRequest.java | 30 +- .../dto/SnsContentCreateResponse.java | 361 +----------------- .../src/main/resources/application.yml | 6 + 14 files changed, 196 insertions(+), 535 deletions(-) create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java diff --git a/smarketing-java/marketing-content/build.gradle b/smarketing-java/marketing-content/build.gradle index 188d7bd..715bc47 100644 --- a/smarketing-java/marketing-content/build.gradle +++ b/smarketing-java/marketing-content/build.gradle @@ -1,4 +1,7 @@ dependencies { implementation project(':common') runtimeOnly 'org.postgresql:postgresql' + + // WebClient를 위한 Spring WebFlux 의존성 + implementation 'org.springframework.boot:spring-boot-starter-webflux' } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/ContentQueryService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/ContentQueryService.java index c196e58..c205bd1 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/ContentQueryService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/ContentQueryService.java @@ -99,24 +99,24 @@ public class ContentQueryService implements ContentQueryUseCase { * @param contentId 콘텐츠 ID * @return 콘텐츠 상세 정보 */ - @Override - public ContentDetailResponse getContentDetail(Long contentId) { - Content content = contentRepository.findById(ContentId.of(contentId)) - .orElseThrow(() -> new BusinessException(ErrorCode.CONTENT_NOT_FOUND)); - - return ContentDetailResponse.builder() - .contentId(content.getId()) - .contentType(content.getContentType().name()) - .platform(content.getPlatform().name()) - .title(content.getTitle()) - .content(content.getContent()) - .hashtags(content.getHashtags()) - .images(content.getImages()) - .status(content.getStatus().name()) - .creationConditions(toCreationConditionsDto(content.getCreationConditions())) - .createdAt(content.getCreatedAt()) - .build(); - } +// @Override +// public ContentDetailResponse getContentDetail(Long contentId) { +// Content content = contentRepository.findById(ContentId.of(contentId)) +// .orElseThrow(() -> new BusinessException(ErrorCode.CONTENT_NOT_FOUND)); +// +// return ContentDetailResponse.builder() +// .contentId(content.getId()) +// .contentType(content.getContentType().name()) +// .platform(content.getPlatform().name()) +// .title(content.getTitle()) +// .content(content.getContent()) +// .hashtags(content.getHashtags()) +// .images(content.getImages()) +// .status(content.getStatus().name()) +// .creationConditions(toCreationConditionsDto(content.getCreationConditions())) +// .createdAt(content.getCreatedAt()) +// .build(); +// } /** * 콘텐츠 삭제 @@ -177,15 +177,15 @@ public class ContentQueryService implements ContentQueryUseCase { * @param conditions CreationConditions 도메인 객체 * @return CreationConditionsDto */ - private ContentDetailResponse.CreationConditionsDto toCreationConditionsDto(CreationConditions conditions) { - if (conditions == null) { - return null; - } - - return ContentDetailResponse.CreationConditionsDto.builder() - .toneAndManner(conditions.getToneAndManner()) - .emotionIntensity(conditions.getEmotionIntensity()) - .eventName(conditions.getEventName()) - .build(); - } +// private ContentDetailResponse.CreationConditionsDto toCreationConditionsDto(CreationConditions conditions) { +// if (conditions == null) { +// return null; +// } +// +// return ContentDetailResponse.CreationConditionsDto.builder() +// .toneAndManner(conditions.getToneAndManner()) +// .emotionIntensity(conditions.getEmotionIntensity()) +// .eventName(conditions.getEventName()) +// .build(); +// } } diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java index 4db4d8a..e89b5c5 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java @@ -49,8 +49,8 @@ public class PosterContentService implements PosterContentUseCase { CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) .requirement(request.getRequirement()) - .toneAndManner(request.getToneAndManner()) - .emotionIntensity(request.getEmotionIntensity()) + // .toneAndManner(request.getToneAndManner()) + // .emotionIntensity(request.getEmotionIntensity()) .eventName(request.getEventName()) .startDate(request.getStartDate()) .endDate(request.getEndDate()) @@ -80,8 +80,8 @@ public class PosterContentService implements PosterContentUseCase { CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) .requirement(request.getRequirement()) - .toneAndManner(request.getToneAndManner()) - .emotionIntensity(request.getEmotionIntensity()) + // .toneAndManner(request.getToneAndManner()) + // .emotionIntensity(request.getEmotionIntensity()) .eventName(request.getEventName()) .startDate(request.getStartDate()) .endDate(request.getEndDate()) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java index dd8e603..af2f388 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java @@ -41,49 +41,12 @@ public class SnsContentService implements SnsContentUseCase { @Transactional public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) { // AI를 사용하여 SNS 콘텐츠 생성 - String generatedContent = aiContentGenerator.generateSnsContent(request); - - // 플랫폼에 맞는 해시태그 생성 - Platform platform = Platform.fromString(request.getPlatform()); - List hashtags = aiContentGenerator.generateHashtags(generatedContent, platform); - - // 생성 조건 정보 구성 - CreationConditions conditions = CreationConditions.builder() - .category(request.getCategory()) - .requirement(request.getRequirement()) - .toneAndManner(request.getToneAndManner()) - .emotionIntensity(request.getEmotionIntensity()) - .eventName(request.getEventName()) - .startDate(request.getStartDate()) - .endDate(request.getEndDate()) - .build(); - - // 임시 콘텐츠 생성 (저장하지 않음) - Content content = Content.builder() -// .contentType(ContentType.SNS_POST) - .platform(platform) - .title(request.getTitle()) - .content(generatedContent) - .hashtags(hashtags) - .images(request.getImages()) - .status(ContentStatus.DRAFT) - .creationConditions(conditions) - .storeId(request.getStoreId()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); + String content = aiContentGenerator.generateSnsContent(request); return SnsContentCreateResponse.builder() - .contentId(null) // 임시 생성이므로 ID 없음 - .contentType(content.getContentType().name()) - .platform(content.getPlatform().name()) - .title(content.getTitle()) - .content(content.getContent()) - .hashtags(content.getHashtags()) - .fixedImages(content.getImages()) - .status(content.getStatus().name()) - .createdAt(content.getCreatedAt()) + .content(content) .build(); + } /** @@ -98,8 +61,8 @@ public class SnsContentService implements SnsContentUseCase { CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) .requirement(request.getRequirement()) - .toneAndManner(request.getToneAndManner()) - .emotionIntensity(request.getEmotionIntensity()) + //.toneAndManner(request.getToneAndManner()) + //.emotionIntensity(request.getEmotionIntensity()) .eventName(request.getEventName()) .startDate(request.getStartDate()) .endDate(request.getEndDate()) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/ContentQueryUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/ContentQueryUseCase.java index 0712961..c63b7c4 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/ContentQueryUseCase.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/ContentQueryUseCase.java @@ -44,7 +44,7 @@ public interface ContentQueryUseCase { * @param contentId 콘텐츠 ID * @return 콘텐츠 상세 정보 */ - ContentDetailResponse getContentDetail(Long contentId); + //ContentDetailResponse getContentDetail(Long contentId); /** * 콘텐츠 삭제 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java new file mode 100644 index 0000000..7f7cf08 --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java @@ -0,0 +1,31 @@ +// marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java +package com.won.smarketing.content.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 org.springframework.http.client.reactive.ReactorClientHttpConnector; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; + +/** + * WebClient 설정 + * Python AI 서비스 호출을 위한 WebClient 구성 + */ +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) + .responseTimeout(Duration.ofMillis(30000)); + + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024)) + .build(); + } +} \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java index d7a9543..a284c2c 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java @@ -24,8 +24,11 @@ public class CreationConditions { private String id; private String category; private String requirement; - private String toneAndManner; - private String emotionIntensity; +// private String toneAndManner; +// private String emotionIntensity; + private String storeName; + private String storeType; + private String target; private String eventName; private LocalDate startDate; private LocalDate endDate; diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiContentGenerator.java index 677853a..ad8f02d 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiContentGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiContentGenerator.java @@ -2,6 +2,7 @@ package com.won.smarketing.content.domain.service; import com.won.smarketing.content.domain.model.Platform; import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest; +import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse; import java.util.List; @@ -18,13 +19,4 @@ public interface AiContentGenerator { * @return 생성된 콘텐츠 */ String generateSnsContent(SnsContentCreateRequest request); - - /** - * 플랫폼별 해시태그 생성 - * - * @param content 콘텐츠 내용 - * @param platform 플랫폼 - * @return 해시태그 목록 - */ - List generateHashtags(String content, Platform platform); } diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java index 9d72f1f..63a5cb8 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java @@ -1,95 +1,99 @@ -// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java package com.won.smarketing.content.infrastructure.external; -// 수정: domain 패키지의 인터페이스를 import import com.won.smarketing.content.domain.service.AiContentGenerator; -import com.won.smarketing.content.domain.model.Platform; import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest; +import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; -import java.util.Arrays; -import java.util.List; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; /** - * Claude AI를 활용한 콘텐츠 생성 구현체 - * Clean Architecture의 Infrastructure Layer에 위치 + * Python AI SNS Content Service를 활용한 콘텐츠 생성 구현체 */ @Component @RequiredArgsConstructor @Slf4j public class ClaudeAiContentGenerator implements AiContentGenerator { + private final WebClient webClient; + + @Value("${external.ai-service.base-url:http://20.249.139.88:5001}") + private String aiServiceBaseUrl; + /** - * SNS 콘텐츠 생성 + * SNS 콘텐츠 생성 - Python AI 서비스 호출 */ @Override public String generateSnsContent(SnsContentCreateRequest request) { - try { - String prompt = buildContentPrompt(request); - return generateDummySnsContent(request.getTitle(), Platform.fromString(request.getPlatform())); - } catch (Exception e) { - log.error("AI 콘텐츠 생성 실패: {}", e.getMessage(), e); - return generateFallbackContent(request.getTitle(), Platform.fromString(request.getPlatform())); + log.info("Python AI 서비스 호출: {}/api/ai/sns", aiServiceBaseUrl); + + // 요청 데이터 구성 + Map requestBody = new HashMap<>(); + requestBody.put("storeId", request.getStoreId()); + requestBody.put("storeName", request.getStoreName()); + requestBody.put("storeType", request.getStoreType()); + requestBody.put("platform", request.getPlatform()); + requestBody.put("title", request.getTitle()); + requestBody.put("category", request.getCategory()); + requestBody.put("contentType", request.getContentType()); + requestBody.put("requirement", request.getRequirement()); + + //requestBody.put("tone_and_manner", request.getToneAndManner()); + // requestBody.put("emotion_intensity", request.getEmotionIntensity()); + requestBody.put("target", request.getTarget()); + + requestBody.put("event_name", request.getEventName()); + requestBody.put("start_date", request.getStartDate()); + requestBody.put("end_date", request.getEndDate()); + + requestBody.put("images", request.getImages()); + + // Python AI 서비스 호출 + Map response = webClient + .method(HttpMethod.GET) + .uri(aiServiceBaseUrl + "/api/ai/sns") + .header("Content-Type", "application/json") + .bodyValue(requestBody) + .retrieve() + .bodyToMono(Map.class) + .timeout(Duration.ofSeconds(30)) + .block(); + + String content = ""; + + // 응답에서 content 추출 + if (response != null && response.containsKey("content")) { + content = (String) response.get("content"); + log.info("AI 서비스 응답 성공: contentLength={}", content.length()); + + return content; } + return content; +// } catch (Exception e) { +// log.error("AI 서비스 호출 실패: {}", e.getMessage(), e); +// return generateFallbackContent(request.getTitle(), Platform.fromString(request.getPlatform())); +// } } /** - * 플랫폼별 해시태그 생성 + * 폴백 콘텐츠 생성 */ - @Override - public List generateHashtags(String content, Platform platform) { - try { - return generateDummyHashtags(platform); - } catch (Exception e) { - log.error("해시태그 생성 실패: {}", e.getMessage(), e); - return generateFallbackHashtags(); - } - } - - private String buildContentPrompt(SnsContentCreateRequest request) { - StringBuilder prompt = new StringBuilder(); - prompt.append("제목: ").append(request.getTitle()).append("\n"); - prompt.append("카테고리: ").append(request.getCategory()).append("\n"); - prompt.append("플랫폼: ").append(request.getPlatform()).append("\n"); - - if (request.getRequirement() != null) { - prompt.append("요구사항: ").append(request.getRequirement()).append("\n"); - } - - if (request.getToneAndManner() != null) { - prompt.append("톤앤매너: ").append(request.getToneAndManner()).append("\n"); - } - - return prompt.toString(); - } - - private String generateDummySnsContent(String title, Platform platform) { - String baseContent = "🌟 " + title + "를 소개합니다! 🌟\n\n" + - "저희 매장에서 특별한 경험을 만나보세요.\n" + - "고객 여러분의 소중한 시간을 더욱 특별하게 만들어드리겠습니다.\n\n"; - - if (platform == Platform.INSTAGRAM) { - return baseContent + "더 많은 정보는 프로필 링크에서 확인하세요! 📸"; - } else { - return baseContent + "자세한 내용은 저희 블로그를 방문해 주세요! ✨"; - } - } - - private String generateFallbackContent(String title, Platform platform) { - return title + "에 대한 멋진 콘텐츠입니다. 많은 관심 부탁드립니다!"; - } - - private List generateDummyHashtags(Platform platform) { - if (platform == Platform.INSTAGRAM) { - return Arrays.asList("#맛집", "#데일리", "#소상공인", "#추천", "#인스타그램"); - } else { - return Arrays.asList("#맛집추천", "#블로그", "#리뷰", "#맛있는곳", "#소상공인응원"); - } - } - - private List generateFallbackHashtags() { - return Arrays.asList("#소상공인", "#마케팅", "#홍보"); - } -} \ No newline at end of file +// private String generateFallbackContent(String title, Platform platform) { +// String baseContent = "🌟 " + title + "를 소개합니다! 🌟\n\n" + +// "저희 매장에서 특별한 경험을 만나보세요.\n" + +// "고객 여러분의 소중한 시간을 더욱 특별하게 만들어드리겠습니다.\n\n"; +// +// if (platform == Platform.INSTAGRAM) { +// return baseContent + "더 많은 정보는 프로필 링크에서 확인하세요! 📸"; +// } else { +// return baseContent + "자세한 내용은 저희 블로그를 방문해 주세요! ✨"; +// } +// } +} diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java index 44fdb68..84836c5 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java @@ -105,8 +105,8 @@ public class ContentMapper { ContentConditionsJpaEntity entity = new ContentConditionsJpaEntity(); entity.setCategory(conditions.getCategory()); entity.setRequirement(conditions.getRequirement()); - entity.setToneAndManner(conditions.getToneAndManner()); - entity.setEmotionIntensity(conditions.getEmotionIntensity()); +// entity.setToneAndManner(conditions.getToneAndManner()); +// entity.setEmotionIntensity(conditions.getEmotionIntensity()); entity.setEventName(conditions.getEventName()); entity.setStartDate(conditions.getStartDate()); entity.setEndDate(conditions.getEndDate()); @@ -126,8 +126,8 @@ public class ContentMapper { return CreationConditions.builder() .category(entity.getCategory()) .requirement(entity.getRequirement()) - .toneAndManner(entity.getToneAndManner()) - .emotionIntensity(entity.getEmotionIntensity()) +// .toneAndManner(entity.getToneAndManner()) +// .emotionIntensity(entity.getEmotionIntensity()) .eventName(entity.getEventName()) .startDate(entity.getStartDate()) .endDate(entity.getEndDate()) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java index 4feb6b7..e527d8a 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java @@ -143,14 +143,14 @@ public class ContentController { * @param contentId 조회할 콘텐츠 ID * @return 콘텐츠 상세 정보 */ - @Operation(summary = "콘텐츠 상세 조회", description = "특정 콘텐츠의 상세 정보를 조회합니다.") - @GetMapping("/{contentId}") - public ResponseEntity> getContentDetail( - @Parameter(description = "콘텐츠 ID", required = true) - @PathVariable Long contentId) { - ContentDetailResponse response = contentQueryUseCase.getContentDetail(contentId); - return ResponseEntity.ok(ApiResponse.success(response)); - } +// @Operation(summary = "콘텐츠 상세 조회", description = "특정 콘텐츠의 상세 정보를 조회합니다.") +// @GetMapping("/{contentId}") +// public ResponseEntity> getContentDetail( +// @Parameter(description = "콘텐츠 ID", required = true) +// @PathVariable Long contentId) { +// ContentDetailResponse response = contentQueryUseCase.getContentDetail(contentId); +// return ResponseEntity.ok(ApiResponse.success(response)); +// } /** * 콘텐츠 삭제 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java index 70235b5..f8bcdeb 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java @@ -1,5 +1,6 @@ package com.won.smarketing.content.presentation.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -23,6 +24,7 @@ import java.util.List; @AllArgsConstructor @Builder @Schema(description = "SNS 콘텐츠 생성 요청") +@JsonIgnoreProperties(ignoreUnknown = true) public class SnsContentCreateRequest { // ==================== 기본 정보 ==================== @@ -31,6 +33,12 @@ public class SnsContentCreateRequest { @NotNull(message = "매장 ID는 필수입니다") private Long storeId; + @Schema(description = "매장 이름", example = "명륜진사갈비") + private String storeName; + + @Schema(description = "업종", example = "한식") + private String storeType; + @Schema(description = "대상 플랫폼", example = "INSTAGRAM", allowableValues = {"INSTAGRAM", "NAVER_BLOG", "FACEBOOK", "KAKAO_STORY"}, @@ -54,15 +62,21 @@ public class SnsContentCreateRequest { @Size(max = 500, message = "요구사항은 500자 이하로 입력해주세요") private String requirement; - @Schema(description = "톤앤매너", - example = "친근함", - allowableValues = {"친근함", "전문적", "유머러스", "감성적", "트렌디"}) - private String toneAndManner; + @Schema(description = "타겟층", example = "10대 청소년") + private String target; - @Schema(description = "감정 강도", - example = "보통", - allowableValues = {"약함", "보통", "강함"}) - private String emotionIntensity; + @Schema(description = "콘텐츠 타입", example = "SNS 게시물") + private String contentType; + +// @Schema(description = "톤앤매너", +// example = "친근함", +// allowableValues = {"친근함", "전문적", "유머러스", "감성적", "트렌디"}) +// private String toneAndManner; + +// @Schema(description = "감정 강도", +// example = "보통", +// allowableValues = {"약함", "보통", "강함"}) +// private String emotionIntensity; // ==================== 이벤트 정보 ==================== diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java index 0acf9ec..541dc4c 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java @@ -1,5 +1,6 @@ package com.won.smarketing.content.presentation.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; @@ -20,364 +21,8 @@ import java.util.List; @AllArgsConstructor @Builder @Schema(description = "SNS 콘텐츠 생성 응답") +@JsonIgnoreProperties(ignoreUnknown = true) public class SnsContentCreateResponse { - - // ==================== 기본 식별 정보 ==================== - - @Schema(description = "생성된 콘텐츠 ID", example = "1") - private Long contentId; - - @Schema(description = "콘텐츠 타입", example = "SNS_POST") - private String contentType; - - @Schema(description = "대상 플랫폼", example = "INSTAGRAM", - allowableValues = {"INSTAGRAM", "NAVER_BLOG", "FACEBOOK", "KAKAO_STORY"}) - private String platform; - - // ==================== AI 생성 콘텐츠 ==================== - - @Schema(description = "AI가 생성한 콘텐츠 제목", - example = "맛있는 신메뉴를 소개합니다! ✨") - private String title; - - @Schema(description = "AI가 생성한 콘텐츠 내용", - example = "안녕하세요! 😊\n\n특별한 신메뉴가 출시되었습니다!\n진짜 맛있어서 꼭 한번 드셔보세요 🍽️\n\n매장에서 기다리고 있을게요! 💫") + @Schema(description = "생성된 콘텐츠") private String content; - - @Schema(description = "AI가 생성한 해시태그 목록", - example = "[\"맛집\", \"신메뉴\", \"추천\", \"인스타그램\", \"일상\", \"좋아요\", \"팔로우\", \"맛있어요\"]") - private List hashtags; - - // ==================== 플랫폼별 최적화 정보 ==================== - - @Schema(description = "플랫폼별 최적화된 콘텐츠 길이", example = "280") - private Integer contentLength; - - @Schema(description = "플랫폼별 권장 해시태그 개수", example = "8") - private Integer recommendedHashtagCount; - - @Schema(description = "플랫폼별 최대 해시태그 개수", example = "15") - private Integer maxHashtagCount; - - // ==================== 생성 조건 정보 ==================== - - @Schema(description = "콘텐츠 생성에 사용된 조건들") - private GenerationConditionsDto generationConditions; - - // ==================== 상태 및 메타데이터 ==================== - - @Schema(description = "생성 상태", example = "DRAFT", - allowableValues = {"DRAFT", "PUBLISHED", "SCHEDULED"}) - private String status; - - @Schema(description = "생성 일시", example = "2024-01-15T10:30:00") - private LocalDateTime createdAt; - - @Schema(description = "AI 모델 버전", example = "gpt-4-turbo") - private String aiModelVersion; - - @Schema(description = "생성 시간 (초)", example = "3.5") - private Double generationTimeSeconds; - - // ==================== 추가 정보 ==================== - - @Schema(description = "업로드된 원본 이미지 URL 목록") - private List originalImages; - - @Schema(description = "콘텐츠 품질 점수 (1-100)", example = "85") - private Integer qualityScore; - - @Schema(description = "예상 참여율 (%)", example = "12.5") - private Double expectedEngagementRate; - - @Schema(description = "콘텐츠 카테고리", example = "음식/메뉴소개") - private String category; - - @Schema(description = "보정된 이미지 URL 목록") - private List fixedImages; - - // ==================== 편집 가능 여부 ==================== - - @Schema(description = "제목 편집 가능 여부", example = "true") - @Builder.Default - private Boolean titleEditable = true; - - @Schema(description = "내용 편집 가능 여부", example = "true") - @Builder.Default - private Boolean contentEditable = true; - - @Schema(description = "해시태그 편집 가능 여부", example = "true") - @Builder.Default - private Boolean hashtagsEditable = true; - - // ==================== 대안 콘텐츠 ==================== - - @Schema(description = "대안 제목 목록 (사용자 선택용)") - private List alternativeTitles; - - @Schema(description = "대안 해시태그 세트 목록") - private List> alternativeHashtagSets; - - // ==================== 내부 DTO 클래스 ==================== - - /** - * 콘텐츠 생성 조건 DTO - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - @Builder - @Schema(description = "콘텐츠 생성 조건") - public static class GenerationConditionsDto { - - @Schema(description = "홍보 대상", example = "메뉴") - private String targetAudience; - - @Schema(description = "이벤트명", example = "신메뉴 출시 이벤트") - private String eventName; - - @Schema(description = "톤앤매너", example = "친근함") - private String toneAndManner; - - @Schema(description = "프로모션 유형", example = "할인 정보") - private String promotionType; - - @Schema(description = "감정 강도", example = "보통") - private String emotionIntensity; - - @Schema(description = "홍보 시작일", example = "2024-01-15T09:00:00") - private LocalDateTime promotionStartDate; - - @Schema(description = "홍보 종료일", example = "2024-01-22T23:59:59") - private LocalDateTime promotionEndDate; - } - - // ==================== 비즈니스 메서드 ==================== - - /** - * 플랫폼별 콘텐츠 최적화 여부 확인 - * - * @return 콘텐츠가 플랫폼 권장 사항을 만족하면 true - */ - public boolean isOptimizedForPlatform() { - if (content == null || hashtags == null) { - return false; - } - - // 플랫폼별 최적화 기준 - switch (platform.toUpperCase()) { - case "INSTAGRAM": - return content.length() <= 2200 && - hashtags.size() <= 15 && - hashtags.size() >= 5; - case "NAVER_BLOG": - return content.length() >= 300 && - hashtags.size() <= 10 && - hashtags.size() >= 3; - case "FACEBOOK": - return content.length() <= 500 && - hashtags.size() <= 5; - default: - return true; - } - } - - /** - * 고품질 콘텐츠 여부 확인 - * - * @return 품질 점수가 80점 이상이면 true - */ - public boolean isHighQuality() { - return qualityScore != null && qualityScore >= 80; - } - - /** - * 참여율 예상 등급 반환 - * - * @return 예상 참여율 등급 (HIGH, MEDIUM, LOW) - */ - public String getExpectedEngagementLevel() { - if (expectedEngagementRate == null) { - return "UNKNOWN"; - } - - if (expectedEngagementRate >= 15.0) { - return "HIGH"; - } else if (expectedEngagementRate >= 8.0) { - return "MEDIUM"; - } else { - return "LOW"; - } - } - - /** - * 해시태그를 문자열로 변환 (# 포함) - * - * @return #으로 시작하는 해시태그 문자열 - */ - public String getHashtagsAsString() { - if (hashtags == null || hashtags.isEmpty()) { - return ""; - } - - return hashtags.stream() - .map(tag -> "#" + tag) - .reduce((a, b) -> a + " " + b) - .orElse(""); - } - - /** - * 콘텐츠 요약 생성 - * - * @param maxLength 최대 길이 - * @return 요약된 콘텐츠 - */ - public String getContentSummary(int maxLength) { - if (content == null || content.length() <= maxLength) { - return content; - } - return content.substring(0, maxLength) + "..."; - } - - /** - * 플랫폼별 최적화 제안사항 반환 - * - * @return 최적화 제안사항 목록 - */ - public List getOptimizationSuggestions() { - List suggestions = new java.util.ArrayList<>(); - - if (!isOptimizedForPlatform()) { - switch (platform.toUpperCase()) { - case "INSTAGRAM": - if (content != null && content.length() > 2200) { - suggestions.add("콘텐츠 길이를 2200자 이하로 줄여주세요."); - } - if (hashtags != null && hashtags.size() > 15) { - suggestions.add("해시태그를 15개 이하로 줄여주세요."); - } - if (hashtags != null && hashtags.size() < 5) { - suggestions.add("해시태그를 5개 이상 추가해주세요."); - } - break; - case "NAVER_BLOG": - if (content != null && content.length() < 300) { - suggestions.add("블로그 포스팅을 위해 내용을 300자 이상으로 늘려주세요."); - } - if (hashtags != null && hashtags.size() > 10) { - suggestions.add("네이버 블로그는 해시태그를 10개 이하로 사용하는 것이 좋습니다."); - } - break; - case "FACEBOOK": - if (content != null && content.length() > 500) { - suggestions.add("페이스북에서는 500자 이하의 짧은 글이 더 효과적입니다."); - } - break; - } - } - - return suggestions; - } - - // ==================== 팩토리 메서드 ==================== - - /** - * 도메인 엔티티에서 SnsContentCreateResponse 생성 - * - * @param content 콘텐츠 도메인 엔티티 - * @param aiMetadata AI 생성 메타데이터 - * @return SnsContentCreateResponse - */ - public static SnsContentCreateResponse fromDomain( - com.won.smarketing.content.domain.model.Content content, - AiGenerationMetadata aiMetadata) { - - SnsContentCreateResponseBuilder builder = SnsContentCreateResponse.builder() - .contentId(content.getId()) - .contentType(content.getContentType().name()) - .platform(content.getPlatform().name()) - .title(content.getTitle()) - .content(content.getContent()) - .hashtags(content.getHashtags()) - .status(content.getStatus().name()) - .createdAt(content.getCreatedAt()) - .originalImages(content.getImages()); - - // 생성 조건 정보 설정 - if (content.getCreationConditions() != null) { - builder.generationConditions(GenerationConditionsDto.builder() - //.targetAudience(content.getCreationConditions().getTargetAudience()) - .eventName(content.getCreationConditions().getEventName()) - .toneAndManner(content.getCreationConditions().getToneAndManner()) - .promotionType(content.getCreationConditions().getPromotionType()) - .emotionIntensity(content.getCreationConditions().getEmotionIntensity()) - .promotionStartDate(content.getPromotionStartDate()) - .promotionEndDate(content.getPromotionEndDate()) - .build()); - } - - // AI 메타데이터 설정 - if (aiMetadata != null) { - builder.aiModelVersion(aiMetadata.getModelVersion()) - .generationTimeSeconds(aiMetadata.getGenerationTime()) - .qualityScore(aiMetadata.getQualityScore()) - .expectedEngagementRate(aiMetadata.getExpectedEngagementRate()) - .alternativeTitles(aiMetadata.getAlternativeTitles()) - .alternativeHashtagSets(aiMetadata.getAlternativeHashtagSets()); - } - - // 플랫폼별 최적화 정보 설정 - SnsContentCreateResponse response = builder.build(); - response.setContentLength(response.getContent() != null ? response.getContent().length() : 0); - response.setRecommendedHashtagCount(getRecommendedHashtagCount(content.getPlatform().name())); - response.setMaxHashtagCount(getMaxHashtagCount(content.getPlatform().name())); - - return response; - } - - /** - * 플랫폼별 권장 해시태그 개수 반환 - */ - private static Integer getRecommendedHashtagCount(String platform) { - switch (platform.toUpperCase()) { - case "INSTAGRAM": return 8; - case "NAVER_BLOG": return 5; - case "FACEBOOK": return 3; - case "KAKAO_STORY": return 5; - default: return 5; - } - } - - /** - * 플랫폼별 최대 해시태그 개수 반환 - */ - private static Integer getMaxHashtagCount(String platform) { - switch (platform.toUpperCase()) { - case "INSTAGRAM": return 15; - case "NAVER_BLOG": return 10; - case "FACEBOOK": return 5; - case "KAKAO_STORY": return 8; - default: return 10; - } - } - - // ==================== AI 생성 메타데이터 DTO ==================== - - /** - * AI 생성 메타데이터 - * AI 생성 과정에서 나온 부가 정보들 - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class AiGenerationMetadata { - private String modelVersion; - private Double generationTime; - private Integer qualityScore; - private Double expectedEngagementRate; - private List alternativeTitles; - private List> alternativeHashtagSets; - private String category; - } } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/resources/application.yml b/smarketing-java/marketing-content/src/main/resources/application.yml index 10dc73d..59b0b54 100644 --- a/smarketing-java/marketing-content/src/main/resources/application.yml +++ b/smarketing-java/marketing-content/src/main/resources/application.yml @@ -2,6 +2,9 @@ server: port: ${SERVER_PORT:8083} spring: + jackson: + deserialization: + fail-on-unknown-properties: false application: name: marketing-content-service datasource: @@ -31,3 +34,6 @@ jwt: logging: level: com.won.smarketing: ${LOG_LEVEL:DEBUG} +external: + ai-service: + base-url: ${AI_SERVICE_BASE_URL:http://20.249.139.88:5001} From 3dd1d0d0ea929feb218588a807cdd803565e3a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:55:41 +0900 Subject: [PATCH 32/33] delete multifile --- .../java/com/won/smarketing/store/dto/MenuUpdateRequest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/MenuUpdateRequest.java b/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/MenuUpdateRequest.java index da0360a..4df4894 100644 --- a/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/MenuUpdateRequest.java +++ b/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/MenuUpdateRequest.java @@ -35,8 +35,4 @@ public class MenuUpdateRequest { @Schema(description = "메뉴 설명", example = "진한 원두의 깊은 맛") private String description; - - @Schema(description = "이미지") - @Size(max = 500, message = "이미지 URL은 500자 이하여야 합니다") - private MultipartFile image; } From 43f8cb7809170dce8bed60ac02b7fdf31039106d Mon Sep 17 00:00:00 2001 From: yuhalog Date: Tue, 17 Jun 2025 10:19:53 +0900 Subject: [PATCH 33/33] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smarketing-java/member/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smarketing-java/member/src/main/resources/application.yml b/smarketing-java/member/src/main/resources/application.yml index 511b56f..6f1de12 100644 --- a/smarketing-java/member/src/main/resources/application.yml +++ b/smarketing-java/member/src/main/resources/application.yml @@ -25,8 +25,8 @@ spring: jwt: secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789} - access-token-validity: ${JWT_ACCESS_VALIDITY:3600000} - refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000} + access-token-validity: ${JWT_ACCESS_VALIDITY:315360000000} + refresh-token-validity: ${JWT_REFRESH_VALIDITY:630720000000} logging: level: