""" SNS 콘텐츠 생성 서비스 (플랫폼 특화 개선) """ import os from typing import Dict, Any, List, Tuple from datetime import datetime from utils.ai_client import AIClient from utils.image_processor import ImageProcessor from models.request_models import SnsContentGetRequest class SnsContentService: def __init__(self): """서비스 초기화""" self.ai_client = AIClient() self.image_processor = ImageProcessor() # 블로그 글 예시 self.blog_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.insta_example = [ { "caption": """힘든 월요일 잘 이겨내신 여러분~~~ 소나기도 내리고 힘드셨을텐데 오늘 하루 고생 많으셨어요~~^^ 고생한 나를 위해 시원한 맥주에 낙곱새~~기가 막히죠??낙지에 대창올리고 그 위에 새우~화룡점정으로 생와사비~ 그 맛은 뭐 말씀 안드려도 여러분들이 더 잘 아실거예요~~그럼 다들 낙곱새 고고~~""", "title": "국민 낙곱새", "summary": "낙곱새 맛집 홍보" }, { "caption": """안녕하세요! 타코몰리김포점입니다! 타코몰리는 멕시코 문화와 풍부한맛을 경험할 수 있는 특별한 공간입니다.🎉 🌶 대표 메뉴를 맛보세요 수제 타코, 바삭한 퀘사디아, 풍성한 부리또로 다양한 맛을 즐길 수 있습니다. 📸 특별한 순간을 담아보세요 #타코몰리김포 해시태그와 함께 여러분의 멋진 사진을 공유해주세요. 이벤트가 기다리고 있답니다!! (새우링/치즈스틱/음료 택1) 📍 위치 김포한강 11로 140번길 15-2 멕시코의 맛과 전통에 푹 빠져보세요! 언제든지 여러분을 기다리고 있겠습니다🌟""", "title": "타코몰리", "summary": "멕시칸 맛집 홍보" }, { "caption":"""📣명륜진사갈비 신메뉴 3종 출시! 특제 고추장 양념에 마늘과 청양고추를 더해 매콤한 불맛이 일품인 #매콤불고기 🌶️ 특제 간장 양념에 마늘과 청양고추를 더해 달콤한 감칠맛이 있는 #달콤불고기 🍯 갈비뼈에 붙어있는 부위로 일반 삼겹살보다 더욱 깊은 맛과 풍미를 가진 #삼겹갈비 까지🍖 신메뉴로 더욱 풍성해진 명륜진사갈비에서 연말 가족/단체모임을 즐겨보세요! ※ 신메뉴는 지점에 따라 탄력적으로 운영되고 있으니, 자세한 문의는 방문하실 매장으로 확인 부탁드립니다.""", "title": "명륜진사갈비", "summary": "갈비 맛집 홍보" } ] # 플랫폼별 콘텐츠 특성 정의 (대폭 개선) self.platform_specs = { '인스타그램': { 'max_length': 2200, 'hashtag_count': 15, 'style': '감성적이고 시각적', 'format': '짧은 문장, 해시태그 활용', 'content_structure': '후킹 문장 → 스토리텔링 → 행동 유도 → 해시태그', 'writing_tips': [ '첫 문장으로 관심 끌기', '이모티콘을 적절히 활용', '줄바꿈으로 가독성 높이기', '개성 있는 말투 사용', '팔로워와의 소통 유도' ], 'hashtag_strategy': [ '브랜딩 해시태그 포함', '지역 기반 해시태그', '트렌딩 해시태그 활용', '음식 관련 인기 해시태그', '감정 표현 해시태그' ], 'call_to_action': ['팔로우', '댓글', '저장', '공유', '방문'] }, '네이버 블로그': { 'max_length': 3000, 'hashtag_count': 10, 'style': '정보성과 친근함', 'format': '구조화된 내용, 상세 설명', 'content_structure': '제목 → 인트로 → 본문(구조화) → 마무리', 'writing_tips': [ '검색 키워드 자연스럽게 포함', '단락별로 소제목 활용', '구체적인 정보 제공', '후기/리뷰 형식 활용', '지역 정보 상세히 기술' ], 'seo_keywords': [ '맛집', '리뷰', '추천', '후기', '메뉴', '가격', '위치', '분위기', '데이트', '모임', '가족', '혼밥' ], 'call_to_action': ['방문', '예약', '문의', '공감', '이웃추가'], 'image_placement_strategy': [ '매장 외관 → 인테리어 → 메뉴판 → 음식 → 분위기', ##'텍스트 2-3문장마다 이미지 배치', '이미지 설명은 간결하고 매력적으로', '마지막에 대표 이미지로 마무리' ] } } # 톤앤매너별 스타일 (플랫폼별 세분화) # self.tone_styles = { # '친근한': { # '인스타그램': '반말, 친구같은 느낌, 이모티콘 많이 사용', # '네이버 블로그': '존댓말이지만 따뜻하고 친근한 어조' # }, # '정중한': { # '인스타그램': '정중하지만 접근하기 쉬운 어조', # '네이버 블로그': '격식 있고 신뢰감 있는 리뷰 스타일' # }, # '재미있는': { # '인스타그램': '유머러스하고 트렌디한 표현', # '네이버 블로그': '재미있는 에피소드가 포함된 후기' # }, # '전문적인': { # '인스타그램': '전문성을 어필하되 딱딱하지 않게', # '네이버 블로그': '전문가 관점의 상세한 분석과 평가' # } # } # 카테고리별 플랫폼 특화 키워드 self.category_keywords = { '음식': { '인스타그램': ['#맛스타그램', '#음식스타그램', '#먹스타그램', '#맛집', '#foodstagram'], '네이버 블로그': ['맛집 리뷰', '음식 후기', '메뉴 추천', '맛집 탐방', '식당 정보'] }, '매장': { '인스타그램': ['#카페스타그램', '#인테리어', '#분위기맛집', '#데이트장소'], '네이버 블로그': ['카페 추천', '분위기 좋은 곳', '인테리어 구경', '모임장소'] }, '이벤트': { '인스타그램': ['#이벤트', '#프로모션', '#할인', '#특가'], '네이버 블로그': ['이벤트 소식', '할인 정보', '프로모션 안내', '특별 혜택'] } } # 감정 강도별 표현 # self.emotion_levels = { # '약함': '은은하고 차분한 표현', # '보통': '적당히 활기찬 표현', # '강함': '매우 열정적이고 강렬한 표현' # } # 이미지 타입 분류를 위한 키워드 self.image_type_keywords = { '매장외관': ['외관', '건물', '간판', '입구', '외부'], '인테리어': ['내부', '인테리어', '좌석', '테이블', '분위기', '장식'], '메뉴판': ['메뉴', '가격', '메뉴판', '메뉴보드', 'menu'], '음식': ['음식', '요리', '메뉴', '디저트', '음료', '플레이팅'], '사람': ['사람', '고객', '직원', '사장', '요리사'], '기타': ['기타', '일반', '전체'] } def generate_sns_content(self, request: SnsContentGetRequest) -> Dict[str, Any]: """ SNS 콘텐츠 생성 (플랫폼별 특화) """ try: # 이미지 다운로드 및 분석 image_analysis = self._analyze_images_from_urls(request.images) # 네이버 블로그인 경우 이미지 배치 계획 생성 image_placement_plan = None if request.platform == '네이버 블로그': image_placement_plan = self._create_image_placement_plan(image_analysis, request) # 플랫폼별 특화 프롬프트 생성 prompt = self._create_platform_specific_prompt(request, image_analysis, image_placement_plan) # blog_example을 프롬프트에 추가 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) # 플랫폼별 후처리 processed_content = self._post_process_content(generated_content, request) # HTML 형식으로 포맷팅 html_content = self._format_to_html(processed_content, request, image_placement_plan) result = { 'success': True, 'content': html_content } # 네이버 블로그인 경우 이미지 배치 가이드라인 추가 if request.platform == '네이버 블로그' and image_placement_plan: result['image_placement_guide'] = image_placement_plan return result except Exception as e: return { 'success': False, 'error': str(e) } def _analyze_images_from_urls(self, image_urls: list) -> Dict[str, Any]: """ URL에서 이미지를 다운로드하고 분석 (이미지 타입 분류 추가) """ analysis_results = [] temp_files = [] try: for i, image_url in enumerate(image_urls): # 이미지 다운로드 temp_path = self.ai_client.download_image_from_url(image_url) if temp_path: temp_files.append(temp_path) # 이미지 분석 try: image_info = self.image_processor.get_image_info(temp_path) image_description = self.ai_client.analyze_image(temp_path) # 이미지 타입 분류 image_type = self._classify_image_type(image_description) analysis_results.append({ 'index': i, 'url': image_url, 'info': image_info, 'description': image_description, 'type': image_type }) except Exception as e: analysis_results.append({ 'index': i, 'url': image_url, 'error': str(e), 'type': '기타' }) return { 'total_images': len(image_urls), 'results': analysis_results } finally: # 임시 파일 정리 for temp_file in temp_files: try: os.remove(temp_file) except: pass def _classify_image_type(self, description: str) -> str: """ 이미지 설명을 바탕으로 이미지 타입 분류 """ description_lower = description.lower() for image_type, keywords in self.image_type_keywords.items(): for keyword in keywords: if keyword in description_lower: return image_type return '기타' def _create_image_placement_plan(self, image_analysis: Dict[str, Any], request: SnsContentGetRequest) -> Dict[ str, Any]: """ 네이버 블로그용 이미지 배치 계획 생성 """ images = image_analysis.get('results', []) if not images: return None # 이미지 타입별 분류 categorized_images = { '매장외관': [], '인테리어': [], '메뉴판': [], '음식': [], '사람': [], '기타': [] } for img in images: img_type = img.get('type', '기타') categorized_images[img_type].append(img) # 블로그 구조에 따른 이미지 배치 계획 placement_plan = { 'structure': [ { 'section': '인트로', 'description': '첫인상과 방문 동기', 'recommended_images': [], 'placement_guide': '매장 외관이나 대표적인 음식 사진으로 시작' }, { 'section': '매장 정보', 'description': '위치, 분위기, 인테리어 소개', 'recommended_images': [], 'placement_guide': '매장 외관 → 내부 인테리어 순서로 배치' }, { 'section': '메뉴 소개', 'description': '주문한 메뉴와 상세 후기', 'recommended_images': [], 'placement_guide': '메뉴판 → 실제 음식 사진 순서로 배치' }, { 'section': '총평', 'description': '재방문 의향과 추천 이유', 'recommended_images': [], 'placement_guide': '가장 매력적인 음식 사진이나 전체 분위기 사진' } ], 'image_sequence': [], 'usage_guide': [] } # 각 섹션에 적절한 이미지 배정 # 인트로: 매장외관 또는 대표 음식 if categorized_images['매장외관']: placement_plan['structure'][0]['recommended_images'].extend(categorized_images['매장외관'][:1]) elif categorized_images['음식']: placement_plan['structure'][0]['recommended_images'].extend(categorized_images['음식'][:1]) # 매장 정보: 외관 + 인테리어 placement_plan['structure'][1]['recommended_images'].extend(categorized_images['매장외관']) placement_plan['structure'][1]['recommended_images'].extend(categorized_images['인테리어']) # 메뉴 소개: 메뉴판 + 음식 placement_plan['structure'][2]['recommended_images'].extend(categorized_images['메뉴판']) placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식']) # 총평: 남은 음식 사진 또는 기타 remaining_food = [img for img in categorized_images['음식'] if img not in placement_plan['structure'][2]['recommended_images']] placement_plan['structure'][3]['recommended_images'].extend(remaining_food[:1]) placement_plan['structure'][3]['recommended_images'].extend(categorized_images['기타'][:1]) # 전체 이미지 순서 생성 for section in placement_plan['structure']: for img in section['recommended_images']: if img not in placement_plan['image_sequence']: placement_plan['image_sequence'].append(img) # 사용 가이드 생성 placement_plan['usage_guide'] = [ "📸 이미지 배치 가이드라인:", "1. 각 섹션마다 2-3문장의 설명 후 이미지 삽입", "2. 이미지마다 간단한 설명 텍스트 추가", "3. 음식 사진은 가장 맛있어 보이는 각도로 배치", "4. 마지막에 전체적인 분위기를 보여주는 사진으로 마무리" ] return placement_plan def _create_platform_specific_prompt(self, request: SnsContentGetRequest, image_analysis: Dict[str, Any], image_placement_plan: Dict[str, Any] = None) -> str: """ 플랫폼별 특화 프롬프트 생성 """ platform_spec = self.platform_specs.get(request.platform, self.platform_specs['인스타그램']) #tone_style = self.tone_styles.get(request.toneAndManner, {}).get(request.platform, '친근하고 자연스러운 어조') # 이미지 설명 추출 image_descriptions = [] for result in image_analysis.get('results', []): if 'description' in result: image_descriptions.append(result['description']) # 플랫폼별 특화 프롬프트 생성 if request.platform == '인스타그램': return self._create_instagram_prompt(request, platform_spec, image_descriptions) elif request.platform == '네이버 블로그': return self._create_naver_blog_prompt(request, platform_spec, image_descriptions, image_placement_plan) else: return self._create_instagram_prompt(request, platform_spec, image_descriptions) def _create_instagram_prompt(self, request: SnsContentGetRequest, platform_spec: dict, image_descriptions: list) -> str: """ 인스타그램 특화 프롬프트 """ category_hashtags = self.category_keywords.get(request.category, {}).get('인스타그램', []) prompt = f""" 당신은 인스타그램 마케팅 전문가입니다. 소상공인 음식점을 위한 매력적인 인스타그램 게시물을 작성해주세요. **🍸 가게 정보:** - 가게명: {request.storeName} - 업종 : {request.storeType} **🎯 콘텐츠 정보:** - 제목: {request.title} - 카테고리: {request.category} - 콘텐츠 타입: {request.contentType} - 메뉴명: {request.menuName or '특별 메뉴'} - 이벤트: {request.eventName or '특별 이벤트'} - 독자층: {request.target} **📱 인스타그램 특화 요구사항:** - 글 구조: {platform_spec['content_structure']} - 최대 길이: {platform_spec['max_length']}자 - 해시태그: {platform_spec['hashtag_count']}개 내외 **✨ 인스타그램 작성 가이드라인:** {chr(10).join([f"- {tip}" for tip in platform_spec['writing_tips']])} **📸 이미지 분석 결과:** {chr(10).join(image_descriptions) if image_descriptions else '시각적으로 매력적인 음식/매장 이미지'} **🏷️ 추천 해시태그 카테고리:** - 기본 해시태그: {', '.join(category_hashtags[:5])} - 브랜딩: #우리가게이름 (실제 가게명으로 대체) - 지역: #강남맛집 #서울카페 (실제 위치로 대체) - 감정: #행복한시간 #맛있다 #추천해요 **💡 콘텐츠 작성 지침:** 1. 첫 문장은 반드시 관심을 끄는 후킹 문장으로 시작 2. 이모티콘을 적절히 활용하여 시각적 재미 추가 3. 스토리텔링을 통해 감정적 연결 유도 4. 명확한 행동 유도 문구 포함 (팔로우, 댓글, 저장, 방문 등) 5. 줄바꿈을 활용하여 가독성 향상 6. 해시태그는 본문과 자연스럽게 연결되도록 배치 **필수 요구사항:** {request.requirement} or '고객의 관심을 끌고 방문을 유도하는 매력적인 게시물' 인스타그램 사용자들이 "저장하고 싶다", "친구에게 공유하고 싶다"라고 생각할 만한 매력적인 게시물을 작성해주세요. 필수 요구사항을 반드시 참고하여 작성해주세요. """ return prompt def _create_naver_blog_prompt(self, request: SnsContentGetRequest, platform_spec: dict, image_descriptions: list, image_placement_plan: Dict[str, Any]) -> str: """ 네이버 블로그 특화 프롬프트 (이미지 배치 계획 포함) """ category_keywords = self.category_keywords.get(request.category, {}).get('네이버 블로그', []) seo_keywords = platform_spec['seo_keywords'] # 이미지 배치 정보 추가 image_placement_info = "" if image_placement_plan: image_placement_info = f""" **📸 이미지 배치 계획:** {chr(10).join([f"- {section['section']}: {section['placement_guide']}" for section in image_placement_plan['structure']])} **이미지 사용 순서:** {chr(10).join([f"{i + 1}. {img.get('description', 'Image')} (타입: {img.get('type', '기타')})" for i, img in enumerate(image_placement_plan.get('image_sequence', []))])} """ prompt = f""" 당신은 네이버 블로그 맛집 리뷰 전문가입니다. 검색 최적화와 정보 제공을 중시하는 네이버 블로그 특성에 맞는 게시물을 작성해주세요. **🍸 가게 정보:** - 가게명: {request.storeName} - 업종 : {request.storeType} **📝 콘텐츠 정보:** - 제목: {request.title} - 카테고리: {request.category} - 콘텐츠 타입: {request.contentType} - 메뉴명: {request.menuName or '대표 메뉴'} - 이벤트: {request.eventName or '특별 이벤트'} - 독자층: {request.target} **🔍 네이버 블로그 특화 요구사항:** - 글 구조: {platform_spec['content_structure']} - 최대 길이: {platform_spec['max_length']}자 - SEO 최적화 필수 **📚 블로그 작성 가이드라인:** {chr(10).join([f"- {tip}" for tip in platform_spec['writing_tips']])} **🖼️ 이미지 분석 결과:** {chr(10).join(image_descriptions) if image_descriptions else '상세한 음식/매장 정보'} {image_placement_info} **🔑 SEO 키워드 (자연스럽게 포함할 것):** - 필수 키워드: {', '.join(seo_keywords[:8])} - 카테고리 키워드: {', '.join(category_keywords[:5])} **💡 콘텐츠 작성 지침:** 1. 검색자의 궁금증을 해결하는 정보 중심 작성 2. 구체적인 가격, 위치, 운영시간 등 실용 정보 포함 3. 개인적인 경험과 솔직한 후기 작성 4. 이미지마다 간단한 설명 문구 추가 5. 지역 정보와 접근성 정보 포함 **필수 요구사항:** {request.requirement} or '유용한 정보를 제공하여 방문을 유도하는 신뢰성 있는 후기' 네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요. 필수 요구사항을 반드시 참고하여 작성해주세요. """ return prompt def _post_process_content(self, content: str, request: SnsContentGetRequest) -> str: """ 플랫폼별 후처리 """ if request.platform == '인스타그램': return self._post_process_instagram(content, request) elif request.platform == '네이버 블로그': return self._post_process_naver_blog(content, request) return content def _post_process_instagram(self, content: str, request: SnsContentGetRequest) -> str: """ 인스타그램 콘텐츠 후처리 """ import re # 해시태그 개수 조정 hashtags = re.findall(r'#[\w가-힣]+', content) if len(hashtags) > 15: # 해시태그가 너무 많으면 중요도 순으로 15개만 유지 all_hashtags = ' '.join(hashtags[:15]) content = re.sub(r'#[\w가-힣]+', '', content) content = content.strip() + '\n\n' + all_hashtags # 이모티콘이 부족하면 추가 emoji_count = content.count('😊') + content.count('🍽️') + content.count('❤️') + content.count('✨') if emoji_count < 3: content = content.replace('!', '! 😊', 1) return content def _post_process_naver_blog(self, content: str, request: SnsContentGetRequest) -> str: """ 네이버 블로그 콘텐츠 후처리 """ # 구조화된 형태로 재구성 if '📍' not in content and '🏷️' not in content: # 이모티콘 기반 구조화가 없으면 추가 lines = content.split('\n') structured_content = [] for line in lines: if '위치' in line or '주소' in line: line = f"📍 {line}" elif '가격' in line or '메뉴' in line: line = f"🏷️ {line}" elif '분위기' in line or '인테리어' in line: line = f"🏠 {line}" structured_content.append(line) content = '\n'.join(structured_content) return content def _format_to_html(self, content: str, request: SnsContentGetRequest, image_placement_plan: Dict[str, Any] = None) -> str: """ 생성된 콘텐츠를 HTML 형식으로 포맷팅 (이미지 배치 포함) """ # 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. 네이버 블로그인 경우 이미지 태그를 실제 이미지로 변환 elif request.platform == '네이버 블로그' and image_placement_plan: content = self._replace_image_tags_with_html(content, image_placement_plan, request.images) # 3. 실제 줄바꿈을
태그로 변환 content = content.replace('\n', '
') # 4. 추가 정리: \r, 여러 공백 정리 content = content.replace('\\r', '').replace('\r', '') # 6. 여러 개의
태그를 하나로 정리 import re content = re.sub(r'(
\s*){3,}', '

', 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 = "" if request.platform == '인스타그램': platform_style = "background: linear-gradient(45deg, #f09433 0%,#e6683c 25%,#dc2743 50%,#cc2366 75%,#bc1888 100%);" elif request.platform == '네이버 블로그': platform_style = "background: linear-gradient(135deg, #1EC800 0%, #00B33C 100%);" else: platform_style = "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);" # 전체 HTML 구조 html_content = f"""

{request.platform} 게시물

{content}
{self._add_metadata_html(request)}
""" return html_content def _replace_image_tags_with_html(self, content: str, image_placement_plan: Dict[str, Any], image_urls: List[str]) -> str: """ 네이버 블로그 콘텐츠의 [IMAGE_X] 태그를 실제 이미지 HTML로 변환 """ import re # [IMAGE_X] 패턴 찾기 image_tags = re.findall(r'\[IMAGE_(\d+)\]', content) for tag in image_tags: image_index = int(tag) - 1 # 1-based to 0-based if image_index < len(image_urls): image_url = image_urls[image_index] # 이미지 배치 계획에서 해당 이미지 정보 찾기 image_info = None for img in image_placement_plan.get('image_sequence', []): if img.get('index') == image_index: image_info = img break # 이미지 설명 생성 image_description = "" if image_info: description = image_info.get('description', '') img_type = image_info.get('type', '기타') if img_type == '음식': image_description = f"😋 {description}" elif img_type == '매장외관': image_description = f"🏪 {description}" elif img_type == '인테리어': image_description = f"🏠 {description}" elif img_type == '메뉴판': image_description = f"📋 {description}" else: image_description = f"📸 {description}" # HTML 이미지 태그로 변환 image_html = f"""
이미지
{image_description}
""" # 콘텐츠에서 태그 교체 content = content.replace(f'[IMAGE_{tag}]', image_html) return content def _add_metadata_html(self, request: SnsContentGetRequest) -> str: """ 메타데이터를 HTML에 추가 """ metadata_html = '
' if request.menuName: metadata_html += f'
메뉴: {request.menuName}
' if request.eventName: metadata_html += f'
이벤트: {request.eventName}
' if request.startDate and request.endDate: metadata_html += f'
기간: {request.startDate} ~ {request.endDate}
' metadata_html += f'
카테고리: {request.category}
' metadata_html += f'
플랫폼: {request.platform}
' metadata_html += f'
생성일: {datetime.now().strftime("%Y-%m-%d %H:%M")}
' metadata_html += '
' return metadata_html