From 163e9d1c024d914bd11eafe57dba744bfad7c646 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Wed, 27 May 2026 13:27:39 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8/=ED=94=8C=EB=9E=9C?= =?UTF-8?q?=EC=97=90=20=EB=B8=8C=EB=9E=9C=EB=93=9C=C2=B7=EC=98=81=EB=AC=B8?= =?UTF-8?q?=EC=B1=84=EB=84=90=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - overrides에 brandAssets·영문 인스타/페북 audit 보장 (채널별 빌더 분리) - logoRules·other_channels·channel_scores 프롬프트 수정, 스키마 입력 필드 추가 Co-Authored-By: Claude Opus 4.7 (1M context) --- app/integrations/llm/schemas/plan.py | 5 ++ app/integrations/llm/schemas/report.py | 6 +++ .../llm/temp-prompt/plan_prompt.txt | 34 ++++++++++++-- .../llm/temp-prompt/report_prompt.txt | 47 +++++++++++++++++++ app/integrations/vision.py | 6 ++- app/services/analysis.py | 11 +++++ 6 files changed, 104 insertions(+), 5 deletions(-) diff --git a/app/integrations/llm/schemas/plan.py b/app/integrations/llm/schemas/plan.py index a12a957..27f46ff 100644 --- a/app/integrations/llm/schemas/plan.py +++ b/app/integrations/llm/schemas/plan.py @@ -15,6 +15,11 @@ class PlanInput(BaseModel): market_keywords: str | None = None market_trend: str | None = None market_target_audience: str | None = None + tiktok: str | None = None + instagram_en: str | None = None + facebook_en: str | None = None + channel_logos: str | None = None + brand_assets: str | None = None # --- BrandGuide --- diff --git a/app/integrations/llm/schemas/report.py b/app/integrations/llm/schemas/report.py index 2b56110..d74ed54 100644 --- a/app/integrations/llm/schemas/report.py +++ b/app/integrations/llm/schemas/report.py @@ -321,6 +321,12 @@ class ReportInput(BaseModel): market_keywords: str | None = None market_trend: str | None = None market_target_audience: str | None = None + branding: str | None = None + brand_assets: str | None = None + tiktok: str | None = None + instagram_en: str | None = None + facebook_en: str | None = None + channel_logos: str | None = None # --- MarketingReport --- diff --git a/app/integrations/llm/temp-prompt/plan_prompt.txt b/app/integrations/llm/temp-prompt/plan_prompt.txt index af7c52e..0b7e757 100644 --- a/app/integrations/llm/temp-prompt/plan_prompt.txt +++ b/app/integrations/llm/temp-prompt/plan_prompt.txt @@ -32,19 +32,47 @@ ## 분석 리포트 {report} +## 추가 채널 데이터 (틱톡 / 인스타그램 EN / 페이스북 EN) +아래에 데이터가 있는 채널은 channelStrategies와 channelBranding에 **반드시 포함**하세요 (틱톡, 영문 인스타그램, 영문 페이스북). null이면 제외. + +### 틱톡 (TikTok) +{tiktok} + +### 인스타그램 (영문 계정) +{instagram_en} + +### 페이스북 (영문 페이지) +{facebook_en} + +## 채널별 로고 분석 (Gemini Vision) — 채널룰/일관성의 근거 +{channel_logos} +- 위 channel_logos[]의 각 항목: channel(채널명), logo_description(프로필이 어떻게 생겼는지), is_official(공식 로고와 일치 여부). +- **channelBranding[]를 이 데이터로 채우세요**: 채널별로 profilePhoto=해당 채널의 logo_description, currentStatus=is_official이 true면 "correct" / false면 "incorrect" (데이터 없는 채널은 "missing"). bannerSpec은 권장 배너 규격(크기/디자인)을 작성. +- **brandInconsistencies[]에 "로고" 항목을 반드시 만드세요**: values[]에 채널마다 channel(채널명) / value(logo_description 그대로) / is_correct(is_official 값) 세 필드를 넣고, impact는 inconsistency_summary, recommendation은 channel_logos.recommendation 기반으로 작성 (공식 로고로 통일 권고 포함). + +## 브랜드 자산 (홈페이지 CSS에서 추출 — 결정적 데이터) +{brand_assets} +- brand_assets.color_palette[]의 hex와 brand_assets.brand_colors(primary/accent/text)는 **홈페이지 CSS에서 실제로 추출한 값**입니다. +- **brandGuide.colors의 hex는 반드시 이 추출값을 그대로 사용하세요. hex를 새로 지어내거나 변형하지 마세요** (매 실행마다 동일해야 함). name/usage 설명은 의미있게 써도 되지만 hex 값 자체는 추출값으로 고정. + ## 섹션별 작성 지침 ### Section 1: brandGuide -- colors: 병원 아이덴티티에 맞는 컬러 팔레트 3~5개 (hex + 사용 가이드) +- colors: **brand_assets.color_palette / brand_colors의 hex를 그대로 사용** (홈페이지 CSS 추출값, 지어내기 금지). 3~5개, 각 hex에 name/usage 부여 - fonts: 제목/본문/캡션용 폰트 시스템 (한글/영문 포함) -- logoRules: DO/DON'T 형식의 로고 사용 규칙 4~6개 +- logoRules: 로고 사용 규칙 4~6개. 각 항목은 rule / description / correct 3개 필드로 구성: + - rule: 규칙을 요약한 **구체적인 제목**. "DO"·"DON'T" 같은 단어를 그대로 넣지 말 것. 실제 규칙 내용을 쓸 것 (예: "보라색+골드 깃털 로고 통일 사용", "모델 사진 프로필 금지", "비공식 변형 로고 사용 금지", "로고 주변 여백 확보"). + - description: 해당 규칙의 상세 설명. + - correct: 권장 규칙(DO)이면 true, 금지/지양 규칙(DON'T)이면 false. 권장(true)과 금지(false)를 섞어서 작성. - toneOfVoice: 브랜드 성격 키워드, 커뮤니케이션 스타일, 권장/지양 표현 예시 - channelBranding: 리포트에 존재하는 채널별 브랜딩 적용 규칙 - brandInconsistencies: 채널 간 브랜딩 불일치 항목 및 개선 권고 ### Section 2: channelStrategies - 리포트에 데이터가 있는 채널만 포함 -- 각 채널의 우선순위(P0/P1/P2), 목표, 콘텐츠 유형, 게시 빈도, 포맷 가이드라인 작성 +- **currentStatus는 현재 채널 상태를 실제 수치로 서술** (예: "14,047 팔로워, Reels 0개", "104K 구독자, 주 2~3회 업로드"). `excellent`/`warning`/`good` 같은 등급·평가어를 절대 쓰지 마세요. +- targetGoal은 구체적 목표 수치로 작성 (예: "50K 팔로워, Reels 주 5개") +- 각 채널의 우선순위(P0/P1/P2), 콘텐츠 유형, 게시 빈도, 포맷 가이드라인 작성 - customerJourneyStage는 해당 채널의 주요 기여 단계로 설정 ### Section 3: contentStrategy diff --git a/app/integrations/llm/temp-prompt/report_prompt.txt b/app/integrations/llm/temp-prompt/report_prompt.txt index 34a9086..2fa59af 100644 --- a/app/integrations/llm/temp-prompt/report_prompt.txt +++ b/app/integrations/llm/temp-prompt/report_prompt.txt @@ -12,6 +12,17 @@ - 시술: {services} - 의료진: {doctors} +## 브랜드 자산 (홈페이지에서 자동 추출) + +### Firecrawl branding (로고 URL / 색상 / 폰트) +{branding} + +### 추출된 브랜드 자산 (로고 묘사 + CSS 색상 팔레트) +{brand_assets} + +⚠️ clinic_snapshot.logo_images / brand_colors는 위 추출값(brand_assets.logo_images, brand_assets.brand_colors)을 **그대로** 사용하세요. hex나 로고 URL을 절대 추측하지 마세요. 추출값이 null이면 해당 필드도 null로 두세요. +로고에 대한 정성 평가(심볼/워드마크/톤)는 brand_assets.logo_description을 근거로 하고, 채널 프로필 이미지가 공식 로고와 일치하는지 판단할 때도 이 묘사를 기준으로 삼으세요. + ## 시장 분석 데이터 ### 경쟁 병원 @@ -43,9 +54,45 @@ ### 강남언니 {gangnam_unni} +### 틱톡 (TikTok) +{tiktok} + +### 인스타그램 (영문 계정) +{instagram_en} + +### 페이스북 (영문 페이지) +{facebook_en} + +### 채널별 로고 분석 (Gemini Vision) +{channel_logos} +- channel_logos.channel_logos[]에 각 채널의 로고 설명(logo_description)과 공식 로고 일치 여부(is_official)가 있습니다. +- **facebook_audit.pages[].logo** 는 짧은 판정 타이틀로: is_official=true면 `"일치 (공식 로고)"`, false면 `"불일치 (비공식 변형)"`. 그리고 **facebook_audit.pages[].logo_description** 에 해당 채널의 logo_description(설명문)을 넣으세요. +- **instagram_audit.accounts[].profile_photo** 는 해당 채널 로고를 짧게 서술 (예: `"모델 사진 (브랜드 로고 아님)"`, `"VIEW 골드 로고"`). 긴 문장 말고 짧게. +- 위 값들은 channel_logos 데이터 기반으로만 작성하고 추측하지 마세요. +- 채널 간 로고 불일치(is_official=false)는 brand 일관성 진단(problem_diagnosis/weaknesses)에 반영하세요. + +## clinic_snapshot / 채널 audit 작성 지침 (수집 데이터 그대로, 추측 금지) +- clinic_snapshot.name 은 {clinic_name} 을 **그대로** 사용 (강남언니 표기명 '-본원' 등으로 바꾸지 말 것). +- clinic_snapshot 의 overall_rating/total_reviews/staff_count/location/certifications/lead_doctor 는 강남언니({gangnam_unni}) 데이터의 값을 그대로 사용. +- instagram_audit.accounts: KR 인스타({instagram})·영문 인스타({instagram_en}) 데이터가 있으면 **각각 별도 계정**으로 넣고, handle/followers/posts/following 은 그 데이터 수치를 그대로. KR=language "KR"·label "인스타그램 KR", EN=language "EN"·label "인스타그램 EN". +- facebook_audit.pages: KR 페북({facebook})·영문 페북({facebook_en}) 데이터가 있으면 **각각 별도 페이지**로 넣고, url/page_name/followers 등은 그 데이터 그대로. language/label 동일 규칙. +- 위 수치·URL·이름은 제공된 데이터에서 그대로 쓰고 절대 지어내지 마세요. + +## 기타 채널 현황 (other_channels) 작성 지침 +- other_channels에는 메인 audit(YouTube/Instagram/Facebook/Website)에 **포함되지 않은** 채널만 넣으세요. +- 위 '채널 데이터'에 **실제 수집된 데이터가 있는 채널만** status=active와 실제 url로 일관되게 포함: 네이버 블로그, 강남언니, 틱톡, 영문 인스타그램({instagram_en}), 영문 페이스북({facebook_en}). +- **영문 인스타그램·영문 페이스북은 KR 메인 audit(Instagram/Facebook)과 별개 계정이므로, 데이터가 있으면 반드시 other_channels에 "Instagram EN" / "Facebook EN"으로 각각 포함하세요 (절대 누락 금지).** +- **수집 데이터에 없는 채널(카카오톡/네이버플레이스/네이버카페/Threads 등)은 절대 임의로 만들지 마세요.** 데이터 없으면 그 채널은 생략 (랜덤 생성·추측 금지). +- url은 수집 데이터의 실제 URL만 사용. 없으면 빈 문자열. + ## 분석 지침 - 점수는 0~100 기준입니다. +- **channel_scores(채널 종합도)에는 데이터가 있는 모든 채널을 각각 별도 항목으로 만드세요. 같은 플랫폼이라도 한국 계정과 영문 계정을 절대 하나로 합치지 마세요:** + - 인스타그램 KR → channel "Instagram", 영문 인스타그램({instagram_en}) 데이터가 있으면 → channel "Instagram EN" (별도 항목) + - 페이스북 KR → channel "Facebook", 영문 페이스북({facebook_en}) 데이터가 있으면 → channel "Facebook EN" (별도 항목) + - 틱톡({tiktok}) 데이터가 있으면 → channel "TikTok" (별도 항목) + - 데이터가 null인 계정은 항목을 만들지 마세요. icon은 instagram/facebook/video 등 플랫폼에 맞게 설정. - strengths와 weaknesses는 각 3개 이상 작성하세요. - roadmap은 우선순위 순으로 실행 가능한 액션으로 작성하세요. - kpis는 실제 수집된 수치 기반으로 현실적인 측정 가능 지표로 작성하세요. diff --git a/app/integrations/vision.py b/app/integrations/vision.py index 67371a2..aea8daf 100644 --- a/app/integrations/vision.py +++ b/app/integrations/vision.py @@ -119,7 +119,8 @@ class VisionClient: ' "logo_text": "로고에 보이는 워드마크 텍스트 그대로 (한글/영문). 없으면 빈 문자열",\n' ' "logo_colors_desc": "로고에 쓰인 색감을 사람이 부르는 이름으로 서술 (예: \'딥네이비 + 골드\'). 정확한 hex는 출력하지 말 것"\n' "}\n" - "주의: 색상 hex 값이나 logo URL 같은 필드는 출력하지 마세요 (별도 추출 로직이 처리)." + "주의: 색상 hex 값이나 logo URL 같은 필드는 출력하지 마세요 (별도 추출 로직이 처리).\n" + "모든 설명/텍스트 값은 반드시 한국어로 작성하세요 (영어 금지)." ) result = await self._ask(urls, prompt) if not result: @@ -166,6 +167,7 @@ class VisionClient: ' "channel_logos": [{"channel": "...", "logo_description": "...", "is_official": true}],\n' ' "inconsistency_summary": "채널 간 로고 일관성 1~2문장 요약",\n' ' "recommendation": "통합 권고 1문장"\n' - "}" + "}\n" + "모든 logo_description·inconsistency_summary·recommendation은 반드시 한국어로 작성하세요 (영어 금지)." ) return await self._ask(urls, prompt) diff --git a/app/services/analysis.py b/app/services/analysis.py index 5c9a9ed..c2b948b 100644 --- a/app/services/analysis.py +++ b/app/services/analysis.py @@ -39,6 +39,12 @@ async def generate_report(analysis_run_id: str) -> ReportOutput: "market_keywords": _json(market.get("keywords")), "market_trend": _json(market.get("trend")), "market_target_audience": _json(market.get("target_audience")), + "branding": _json(clinic.get("branding")), + "brand_assets": _json(clinic.get("brandAssets")), + "tiktok": _json(clinic.get("tiktok")), + "instagram_en": _json(clinic.get("instagramEn")), + "facebook_en": _json(clinic.get("facebookEn")), + "channel_logos": _json(clinic.get("channelLogos")), **{ channel: _json(data) for channel, data in raw.items() @@ -78,6 +84,11 @@ async def generate_plan(analysis_run_id: str) -> PlanOutput: "market_keywords": _json(market.get("keywords")), "market_trend": _json(market.get("trend")), "market_target_audience": _json(market.get("target_audience")), + "tiktok": _json(clinic.get("tiktok")), + "instagram_en": _json(clinic.get("instagramEn")), + "facebook_en": _json(clinic.get("facebookEn")), + "channel_logos": _json(clinic.get("channelLogos")), + "brand_assets": _json(clinic.get("brandAssets")), } return await LLMService(provider="perplexity").generate(plan_prompt, input_data)