Compare commits
No commits in common. "42e09ae2d153897ce284a958c9d27c04b1329a36" and "c1f39aceff206a61127b13ce0104087a6d35d9ed" have entirely different histories.
42e09ae2d1
...
c1f39aceff
|
|
@ -1,6 +1,6 @@
|
||||||
from .clinics import router as clinics_router
|
from .clinics import router as clinics_router
|
||||||
from .analysis import router as analysis_router
|
from .analysis import router as analysis_router
|
||||||
from .report import router as report_router
|
from .reports import router as reports_router
|
||||||
from .plan import router as plan_router
|
from .plans import router as plans_router
|
||||||
|
|
||||||
routers = [clinics_router, analysis_router, report_router, plan_router]
|
routers = [clinics_router, analysis_router, reports_router, plans_router]
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
|
||||||
from common.db import fetchone
|
|
||||||
from common.deps import verify_api_key
|
|
||||||
from integrations.llm.schemas.plan import PlanOutput
|
|
||||||
from models.plan import PlanApiResponse
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/plan", tags=["plan"], dependencies=[Depends(verify_api_key)])
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{run_id}", response_model=PlanApiResponse, response_model_by_alias=True)
|
|
||||||
async def get_plan(run_id: str):
|
|
||||||
logger.info("GET /api/plan/%s", run_id)
|
|
||||||
row = await fetchone(
|
|
||||||
"SELECT ar.plan_data, ar.created_at, h.hospital_name, h.hospital_name_en, h.url"
|
|
||||||
" FROM analysis_runs ar"
|
|
||||||
" JOIN hospital_baseinfo h ON ar.hospital_id = h.hospital_id"
|
|
||||||
" WHERE ar.analysis_run_id = %s",
|
|
||||||
(run_id,),
|
|
||||||
)
|
|
||||||
if row is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Run not found")
|
|
||||||
if row["plan_data"] is None:
|
|
||||||
return Response(status_code=204)
|
|
||||||
data = json.loads(row["plan_data"]) if isinstance(row["plan_data"], str) else row["plan_data"]
|
|
||||||
plan = PlanOutput(**data)
|
|
||||||
return PlanApiResponse(
|
|
||||||
id=run_id,
|
|
||||||
clinic_name=row["hospital_name"],
|
|
||||||
clinic_name_en=row["hospital_name_en"],
|
|
||||||
created_at=str(row["created_at"]),
|
|
||||||
target_url=row["url"],
|
|
||||||
**plan.model_dump(),
|
|
||||||
)
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||||
|
from common.db import fetchone
|
||||||
|
from common.deps import verify_api_key
|
||||||
|
from integrations.llm.schemas.plan import PlanOutput
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/plans", tags=["plans"], dependencies=[Depends(verify_api_key)])
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{run_id}", response_model=PlanOutput | None)
|
||||||
|
async def get_plan(run_id: str):
|
||||||
|
logger.info("GET /api/plans/%s", run_id)
|
||||||
|
row = await fetchone("SELECT plan_data FROM analysis_runs WHERE analysis_run_id = %s", (run_id,))
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Run not found")
|
||||||
|
if row["plan_data"] is None:
|
||||||
|
return Response(status_code=204)
|
||||||
|
data = json.loads(row["plan_data"]) if isinstance(row["plan_data"], str) else row["plan_data"]
|
||||||
|
return PlanOutput(**data)
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
|
||||||
from common.db import fetchone
|
|
||||||
from common.deps import verify_api_key
|
|
||||||
from integrations.llm.schemas.report import ReportOutput
|
|
||||||
from models.report import MarketingReportResponse
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/report", tags=["report"], dependencies=[Depends(verify_api_key)])
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{run_id}", response_model=MarketingReportResponse, response_model_by_alias=True)
|
|
||||||
async def get_report(run_id: str):
|
|
||||||
logger.info("GET /api/report/%s", run_id)
|
|
||||||
row = await fetchone(
|
|
||||||
"SELECT ar.report_data, ar.created_at, h.hospital_name, h.hospital_name_en, h.url"
|
|
||||||
" FROM analysis_runs ar"
|
|
||||||
" JOIN hospital_baseinfo h ON ar.hospital_id = h.hospital_id"
|
|
||||||
" WHERE ar.analysis_run_id = %s",
|
|
||||||
(run_id,),
|
|
||||||
)
|
|
||||||
if row is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Run not found")
|
|
||||||
if row["report_data"] is None:
|
|
||||||
return Response(status_code=204)
|
|
||||||
data = json.loads(row["report_data"]) if isinstance(row["report_data"], str) else row["report_data"]
|
|
||||||
llm_output = ReportOutput(**data)
|
|
||||||
return MarketingReportResponse(
|
|
||||||
id=run_id,
|
|
||||||
clinic_name=row["hospital_name"],
|
|
||||||
clinic_name_en=row["hospital_name_en"],
|
|
||||||
created_at=str(row["created_at"]),
|
|
||||||
target_url=row["url"],
|
|
||||||
**llm_output.model_dump(exclude={"id", "created_at", "target_url"}),
|
|
||||||
)
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||||
|
from common.db import fetchone
|
||||||
|
from common.deps import verify_api_key
|
||||||
|
from integrations.llm.schemas.report import ReportOutput
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/reports", tags=["reports"], dependencies=[Depends(verify_api_key)])
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{run_id}", response_model=ReportOutput | None)
|
||||||
|
async def get_report(run_id: str):
|
||||||
|
logger.info("GET /api/reports/%s", run_id)
|
||||||
|
row = await fetchone(
|
||||||
|
"SELECT report_data FROM analysis_runs WHERE analysis_run_id = %s",
|
||||||
|
(run_id,),
|
||||||
|
)
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Run not found")
|
||||||
|
if row["report_data"] is None:
|
||||||
|
return Response(status_code=204)
|
||||||
|
data = json.loads(row["report_data"]) if isinstance(row["report_data"], str) else row["report_data"]
|
||||||
|
return ReportOutput(**data)
|
||||||
|
|
@ -7,7 +7,6 @@ import httpx
|
||||||
REQUEST_TIMEOUT = 60
|
REQUEST_TIMEOUT = 60
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_env(key: str) -> str:
|
def get_env(key: str) -> str:
|
||||||
v = os.environ.get(key, "")
|
v = os.environ.get(key, "")
|
||||||
if not v:
|
if not v:
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ class LLMService:
|
||||||
response = await self.client.chat.completions.create(
|
response = await self.client.chat.completions.create(
|
||||||
model=prompt.model,
|
model=prompt.model,
|
||||||
messages=[{"role": "user", "content": prompt_text}],
|
messages=[{"role": "user", "content": prompt_text}],
|
||||||
max_tokens=16000,
|
|
||||||
response_format={
|
response_format={
|
||||||
"type": "json_schema",
|
"type": "json_schema",
|
||||||
"json_schema": {"name": prompt.output_class.__name__, "schema": prompt.output_class.model_json_schema()},
|
"json_schema": {"name": prompt.output_class.__name__, "schema": prompt.output_class.model_json_schema()},
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class FontSpec(BaseModel):
|
||||||
family: str
|
family: str
|
||||||
weight: str
|
weight: str
|
||||||
usage: str
|
usage: str
|
||||||
sample_text: str
|
sampleText: str
|
||||||
|
|
||||||
|
|
||||||
class LogoUsageRule(BaseModel):
|
class LogoUsageRule(BaseModel):
|
||||||
|
|
@ -36,29 +36,29 @@ class LogoUsageRule(BaseModel):
|
||||||
|
|
||||||
class ToneOfVoice(BaseModel):
|
class ToneOfVoice(BaseModel):
|
||||||
personality: list[str]
|
personality: list[str]
|
||||||
communication_style: str
|
communicationStyle: str
|
||||||
do_examples: list[str]
|
doExamples: list[str]
|
||||||
dont_examples: list[str]
|
dontExamples: list[str]
|
||||||
|
|
||||||
|
|
||||||
class ChannelBrandingRule(BaseModel):
|
class ChannelBrandingRule(BaseModel):
|
||||||
channel: str
|
channel: str
|
||||||
icon: str
|
icon: str
|
||||||
profile_photo: str
|
profilePhoto: str
|
||||||
banner_spec: str
|
bannerSpec: str
|
||||||
bio_template: str
|
bioTemplate: str
|
||||||
current_status: Literal["correct", "incorrect", "missing"]
|
currentStatus: Literal["correct", "incorrect", "missing"]
|
||||||
|
|
||||||
|
|
||||||
class BrandPlanInconsistencyValue(BaseModel):
|
class BrandInconsistencyValue(BaseModel):
|
||||||
channel: str
|
channel: str
|
||||||
value: str
|
value: str
|
||||||
is_correct: bool
|
isCorrect: bool
|
||||||
|
|
||||||
|
|
||||||
class BrandPlanInconsistency(BaseModel):
|
class BrandInconsistency(BaseModel):
|
||||||
field: str
|
field: str
|
||||||
values: list[BrandPlanInconsistencyValue]
|
values: list[BrandInconsistencyValue]
|
||||||
impact: str
|
impact: str
|
||||||
recommendation: str
|
recommendation: str
|
||||||
|
|
||||||
|
|
@ -66,26 +66,26 @@ class BrandPlanInconsistency(BaseModel):
|
||||||
class BrandGuide(BaseModel):
|
class BrandGuide(BaseModel):
|
||||||
colors: list[ColorSwatch]
|
colors: list[ColorSwatch]
|
||||||
fonts: list[FontSpec]
|
fonts: list[FontSpec]
|
||||||
logo_rules: list[LogoUsageRule]
|
logoRules: list[LogoUsageRule]
|
||||||
tone_of_voice: ToneOfVoice
|
toneOfVoice: ToneOfVoice
|
||||||
channel_branding: list[ChannelBrandingRule]
|
channelBranding: list[ChannelBrandingRule]
|
||||||
brand_inconsistencies: list[BrandPlanInconsistency]
|
brandInconsistencies: list[BrandInconsistency]
|
||||||
|
|
||||||
|
|
||||||
# --- ChannelStrategy ---
|
# --- ChannelStrategy ---
|
||||||
|
|
||||||
class ChannelStrategyCard(BaseModel):
|
class ChannelStrategyCard(BaseModel):
|
||||||
channel_id: str
|
channelId: str
|
||||||
channel_name: str
|
channelName: str
|
||||||
icon: str
|
icon: str
|
||||||
current_status: str
|
currentStatus: str
|
||||||
target_goal: str
|
targetGoal: str
|
||||||
content_types: list[str]
|
contentTypes: list[str]
|
||||||
posting_frequency: str
|
postingFrequency: str
|
||||||
tone: str
|
tone: str
|
||||||
format_guidelines: list[str]
|
formatGuidelines: list[str]
|
||||||
priority: Literal["P0", "P1", "P2"]
|
priority: Literal["P0", "P1", "P2"]
|
||||||
customer_journey_stage: Literal["awareness", "interest", "consideration", "conversion", "loyalty"] | None = None
|
customerJourneyStage: Literal["awareness", "interest", "consideration", "conversion", "loyalty"] | None = None
|
||||||
|
|
||||||
|
|
||||||
# --- ContentStrategy ---
|
# --- ContentStrategy ---
|
||||||
|
|
@ -93,8 +93,8 @@ class ChannelStrategyCard(BaseModel):
|
||||||
class ContentPillar(BaseModel):
|
class ContentPillar(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
related_usp: str
|
relatedUSP: str
|
||||||
example_topics: list[str]
|
exampleTopics: list[str]
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -121,30 +121,30 @@ class RepurposingOutput(BaseModel):
|
||||||
|
|
||||||
class ContentStrategyData(BaseModel):
|
class ContentStrategyData(BaseModel):
|
||||||
pillars: list[ContentPillar]
|
pillars: list[ContentPillar]
|
||||||
type_matrix: list[ContentTypeRow]
|
typeMatrix: list[ContentTypeRow]
|
||||||
workflow: list[WorkflowStep]
|
workflow: list[WorkflowStep]
|
||||||
repurposing_source: str
|
repurposingSource: str
|
||||||
repurposing_outputs: list[RepurposingOutput]
|
repurposingOutputs: list[RepurposingOutput]
|
||||||
|
|
||||||
|
|
||||||
# --- Calendar ---
|
# --- Calendar ---
|
||||||
|
|
||||||
class CalendarEntry(BaseModel):
|
class CalendarEntry(BaseModel):
|
||||||
day_of_week: int
|
dayOfWeek: int
|
||||||
channel: str
|
channel: str
|
||||||
channel_icon: str
|
channelIcon: str
|
||||||
content_type: Literal["video", "blog", "social", "ad"]
|
contentType: Literal["video", "blog", "social", "ad"]
|
||||||
title: str
|
title: str
|
||||||
id: str | None = None
|
id: str | None = None
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
pillar: str | None = None
|
pillar: str | None = None
|
||||||
status: Literal["draft", "approved", "published"] | None = None
|
status: Literal["draft", "approved", "published"] | None = None
|
||||||
is_manual_edit: bool | None = None
|
isManualEdit: bool | None = None
|
||||||
ai_prompt_seed: str | None = None
|
aiPromptSeed: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class CalendarWeek(BaseModel):
|
class CalendarWeek(BaseModel):
|
||||||
week_number: int
|
weekNumber: int
|
||||||
label: str
|
label: str
|
||||||
entries: list[CalendarEntry]
|
entries: list[CalendarEntry]
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ class ContentCountSummary(BaseModel):
|
||||||
|
|
||||||
class CalendarData(BaseModel):
|
class CalendarData(BaseModel):
|
||||||
weeks: list[CalendarWeek]
|
weeks: list[CalendarWeek]
|
||||||
monthly_summary: list[ContentCountSummary]
|
monthlySummary: list[ContentCountSummary]
|
||||||
|
|
||||||
|
|
||||||
# --- AssetCollection ---
|
# --- AssetCollection ---
|
||||||
|
|
@ -166,11 +166,11 @@ class CalendarData(BaseModel):
|
||||||
class AssetCard(BaseModel):
|
class AssetCard(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
source: Literal["homepage", "naver_place", "blog", "social", "youtube"]
|
source: Literal["homepage", "naver_place", "blog", "social", "youtube"]
|
||||||
source_label: str
|
sourceLabel: str
|
||||||
type: Literal["photo", "video", "text"]
|
type: Literal["photo", "video", "text"]
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
repurposing_suggestions: list[str]
|
repurposingSuggestions: list[str]
|
||||||
status: Literal["collected", "pending", "needs_creation"]
|
status: Literal["collected", "pending", "needs_creation"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -178,29 +178,62 @@ class YouTubeRepurposeItem(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
views: int
|
views: int
|
||||||
type: Literal["Short", "Long"]
|
type: Literal["Short", "Long"]
|
||||||
repurpose_as: list[str]
|
repurposeAs: list[str]
|
||||||
|
|
||||||
|
|
||||||
class AssetCollectionData(BaseModel):
|
class AssetCollectionData(BaseModel):
|
||||||
assets: list[AssetCard]
|
assets: list[AssetCard]
|
||||||
youtube_repurpose: list[YouTubeRepurposeItem]
|
youtubeRepurpose: list[YouTubeRepurposeItem]
|
||||||
|
|
||||||
|
|
||||||
# --- Repurposing ---
|
# --- Repurposing ---
|
||||||
|
|
||||||
class RepurposingProposalItem(BaseModel):
|
class RepurposingProposalItem(BaseModel):
|
||||||
source_video: YouTubeRepurposeItem
|
sourceVideo: YouTubeRepurposeItem
|
||||||
outputs: list[RepurposingOutput]
|
outputs: list[RepurposingOutput]
|
||||||
estimated_effort: Literal["low", "medium", "high"]
|
estimatedEffort: Literal["low", "medium", "high"]
|
||||||
priority: Literal["high", "medium", "low"]
|
priority: Literal["high", "medium", "low"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Workflow ---
|
||||||
|
|
||||||
|
class WorkflowVideoDraft(BaseModel):
|
||||||
|
script: str
|
||||||
|
shootingGuide: list[str]
|
||||||
|
duration: str
|
||||||
|
|
||||||
|
|
||||||
|
# class WorkflowImageTextDraft(BaseModel):
|
||||||
|
# type: Literal["cardnews", "blog"]
|
||||||
|
# headline: str
|
||||||
|
# copy: list[str]
|
||||||
|
# layoutHint: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# class WorkflowItem(BaseModel):
|
||||||
|
# id: str
|
||||||
|
# title: str
|
||||||
|
# contentType: Literal["video", "image-text"]
|
||||||
|
# channel: str
|
||||||
|
# channelIcon: str
|
||||||
|
# stage: Literal["planning", "ai-draft", "review", "approved", "scheduled"]
|
||||||
|
# userNotes: str | None = None
|
||||||
|
# videoDraft: WorkflowVideoDraft | None = None
|
||||||
|
# imageTextDraft: WorkflowImageTextDraft | None = None
|
||||||
|
# scheduledDate: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# class WorkflowData(BaseModel):
|
||||||
|
# items: list[WorkflowItem]
|
||||||
|
|
||||||
|
|
||||||
# --- PlanOutput ---
|
# --- PlanOutput ---
|
||||||
|
|
||||||
class PlanOutput(BaseModel):
|
class PlanOutput(BaseModel):
|
||||||
brand_guide: BrandGuide
|
brandGuide: BrandGuide
|
||||||
channel_strategies: list[ChannelStrategyCard]
|
channelStrategies: list[ChannelStrategyCard]
|
||||||
content_strategy: ContentStrategyData
|
contentStrategy: ContentStrategyData
|
||||||
calendar: CalendarData
|
calendar: CalendarData
|
||||||
asset_collection: AssetCollectionData
|
assetCollection: AssetCollectionData
|
||||||
repurposing_proposals: list[RepurposingProposalItem] | None = None
|
repurposingProposals: list[RepurposingProposalItem] | None = None
|
||||||
|
#workflow: WorkflowData | None = None
|
||||||
|
|
|
||||||
|
|
@ -1,309 +1,8 @@
|
||||||
from __future__ import annotations
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from models.status import Severity, ChannelStatus, DataSource, Language, VideoType, AnnotationType
|
|
||||||
|
|
||||||
|
|
||||||
class ScreenshotAnnotation(BaseModel):
|
# template.format(**model_dump()) 에 삽입될 변수들
|
||||||
type: AnnotationType
|
# 각 채널 raw_data를 호출부에서 json.dumps()로 직렬화해서 넘겨야 함
|
||||||
x: float
|
|
||||||
y: float
|
|
||||||
width: float | None = None
|
|
||||||
height: float | None = None
|
|
||||||
label: str | None = None
|
|
||||||
color: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ScreenshotEvidence(BaseModel):
|
|
||||||
id: str
|
|
||||||
url: str
|
|
||||||
channel: str
|
|
||||||
captured_at: str
|
|
||||||
caption: str
|
|
||||||
source_url: str | None = None
|
|
||||||
annotations: list[ScreenshotAnnotation] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class DiagnosisItem(BaseModel):
|
|
||||||
category: str
|
|
||||||
detail: str
|
|
||||||
severity: Severity
|
|
||||||
evidence_ids: list[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# --- ClinicSnapshot ---
|
|
||||||
|
|
||||||
class LeadDoctor(BaseModel):
|
|
||||||
name: str
|
|
||||||
credentials: str
|
|
||||||
rating: float
|
|
||||||
review_count: int
|
|
||||||
|
|
||||||
|
|
||||||
class PriceRange(BaseModel):
|
|
||||||
min: str
|
|
||||||
max: str
|
|
||||||
currency: str
|
|
||||||
|
|
||||||
|
|
||||||
class LogoImages(BaseModel):
|
|
||||||
circle: str | None = None
|
|
||||||
horizontal: str | None = None
|
|
||||||
korean: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class BrandColors(BaseModel):
|
|
||||||
primary: str
|
|
||||||
accent: str
|
|
||||||
text: str
|
|
||||||
|
|
||||||
|
|
||||||
class RegistryData(BaseModel):
|
|
||||||
district: str | None = None
|
|
||||||
branches: str | None = None
|
|
||||||
brand_group: str | None = None
|
|
||||||
website_en: str | None = None
|
|
||||||
naver_place_url: str | None = None
|
|
||||||
gangnam_unni_url: str | None = None
|
|
||||||
google_maps_url: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ClinicSnapshot(BaseModel):
|
|
||||||
name: str
|
|
||||||
name_en: str
|
|
||||||
established: str
|
|
||||||
years_in_business: int
|
|
||||||
staff_count: int
|
|
||||||
lead_doctor: LeadDoctor
|
|
||||||
overall_rating: float
|
|
||||||
total_reviews: int
|
|
||||||
price_range: PriceRange
|
|
||||||
certifications: list[str]
|
|
||||||
media_appearances: list[str]
|
|
||||||
medical_tourism: list[str]
|
|
||||||
location: str
|
|
||||||
nearest_station: str
|
|
||||||
phone: str
|
|
||||||
domain: str
|
|
||||||
logo_images: LogoImages | None = None
|
|
||||||
brand_colors: BrandColors | None = None
|
|
||||||
source: DataSource | None = None
|
|
||||||
registry_data: RegistryData | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# --- ChannelScore ---
|
|
||||||
|
|
||||||
class ChannelScore(BaseModel):
|
|
||||||
channel: str
|
|
||||||
icon: str
|
|
||||||
score: int
|
|
||||||
max_score: int
|
|
||||||
status: Severity
|
|
||||||
headline: str
|
|
||||||
|
|
||||||
|
|
||||||
# --- YouTube ---
|
|
||||||
|
|
||||||
class WeeklyViewGrowth(BaseModel):
|
|
||||||
absolute: int
|
|
||||||
percentage: float
|
|
||||||
|
|
||||||
|
|
||||||
class EstimatedRevenue(BaseModel):
|
|
||||||
min: int
|
|
||||||
max: int
|
|
||||||
|
|
||||||
|
|
||||||
class LinkedUrl(BaseModel):
|
|
||||||
label: str
|
|
||||||
url: str
|
|
||||||
|
|
||||||
|
|
||||||
class TopVideo(BaseModel):
|
|
||||||
title: str
|
|
||||||
views: int
|
|
||||||
uploaded_ago: str
|
|
||||||
type: VideoType
|
|
||||||
duration: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class YouTubeAudit(BaseModel):
|
|
||||||
channel_name: str
|
|
||||||
handle: str
|
|
||||||
subscribers: int
|
|
||||||
total_videos: int
|
|
||||||
total_views: int
|
|
||||||
weekly_view_growth: WeeklyViewGrowth
|
|
||||||
estimated_monthly_revenue: EstimatedRevenue
|
|
||||||
avg_video_length: str
|
|
||||||
upload_frequency: str
|
|
||||||
channel_created_date: str
|
|
||||||
subscriber_rank: str
|
|
||||||
channel_description: str
|
|
||||||
linked_urls: list[LinkedUrl]
|
|
||||||
playlists: list[str]
|
|
||||||
top_videos: list[TopVideo]
|
|
||||||
diagnosis: list[DiagnosisItem]
|
|
||||||
|
|
||||||
|
|
||||||
# --- Instagram ---
|
|
||||||
|
|
||||||
class InstagramAccount(BaseModel):
|
|
||||||
handle: str
|
|
||||||
language: Language
|
|
||||||
label: str
|
|
||||||
posts: int
|
|
||||||
followers: int
|
|
||||||
following: int
|
|
||||||
category: str
|
|
||||||
profile_link: str
|
|
||||||
highlights: list[str]
|
|
||||||
reels_count: int
|
|
||||||
content_format: str
|
|
||||||
profile_photo: str
|
|
||||||
bio: str
|
|
||||||
|
|
||||||
|
|
||||||
class InstagramAudit(BaseModel):
|
|
||||||
accounts: list[InstagramAccount]
|
|
||||||
diagnosis: list[DiagnosisItem]
|
|
||||||
|
|
||||||
|
|
||||||
# --- Facebook ---
|
|
||||||
|
|
||||||
class BrandInconsistencyValue(BaseModel):
|
|
||||||
channel: str
|
|
||||||
value: str
|
|
||||||
is_correct: bool
|
|
||||||
|
|
||||||
|
|
||||||
class BrandInconsistency(BaseModel):
|
|
||||||
field: str
|
|
||||||
values: list[BrandInconsistencyValue]
|
|
||||||
impact: str
|
|
||||||
recommendation: str
|
|
||||||
|
|
||||||
|
|
||||||
class FacebookPage(BaseModel):
|
|
||||||
url: str
|
|
||||||
page_name: str
|
|
||||||
language: Language
|
|
||||||
label: str
|
|
||||||
followers: int
|
|
||||||
following: int
|
|
||||||
category: str
|
|
||||||
bio: str
|
|
||||||
logo: str
|
|
||||||
logo_description: str
|
|
||||||
link: str
|
|
||||||
linked_domain: str
|
|
||||||
reviews: int
|
|
||||||
recent_post_age: str
|
|
||||||
has_whatsapp: bool
|
|
||||||
post_frequency: str | None = None
|
|
||||||
top_content_type: str | None = None
|
|
||||||
engagement: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class FacebookAudit(BaseModel):
|
|
||||||
pages: list[FacebookPage]
|
|
||||||
diagnosis: list[DiagnosisItem]
|
|
||||||
brand_inconsistencies: list[BrandInconsistency]
|
|
||||||
consolidation_recommendation: str
|
|
||||||
|
|
||||||
|
|
||||||
# --- 기타 채널 / 웹사이트 ---
|
|
||||||
|
|
||||||
class OtherChannel(BaseModel):
|
|
||||||
name: str
|
|
||||||
status: ChannelStatus
|
|
||||||
details: str
|
|
||||||
url: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TrackingPixel(BaseModel):
|
|
||||||
name: str
|
|
||||||
installed: bool
|
|
||||||
details: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class SnsLink(BaseModel):
|
|
||||||
platform: str
|
|
||||||
url: str
|
|
||||||
location: str
|
|
||||||
|
|
||||||
|
|
||||||
class AdditionalDomain(BaseModel):
|
|
||||||
domain: str
|
|
||||||
purpose: str
|
|
||||||
|
|
||||||
|
|
||||||
class WebsiteAudit(BaseModel):
|
|
||||||
primary_domain: str
|
|
||||||
additional_domains: list[AdditionalDomain]
|
|
||||||
sns_links_on_site: bool
|
|
||||||
sns_links_detail: list[SnsLink] | None = None
|
|
||||||
tracking_pixels: list[TrackingPixel]
|
|
||||||
main_cta: str
|
|
||||||
|
|
||||||
|
|
||||||
# --- Transformation ---
|
|
||||||
|
|
||||||
class AsIsToBeItem(BaseModel):
|
|
||||||
area: str
|
|
||||||
as_is: str
|
|
||||||
to_be: str
|
|
||||||
|
|
||||||
|
|
||||||
class StrategyDetail(BaseModel):
|
|
||||||
strategy: str
|
|
||||||
detail: str
|
|
||||||
|
|
||||||
|
|
||||||
class PlatformStrategy(BaseModel):
|
|
||||||
platform: str
|
|
||||||
icon: str
|
|
||||||
current_metric: str
|
|
||||||
target_metric: str
|
|
||||||
strategies: list[StrategyDetail]
|
|
||||||
|
|
||||||
|
|
||||||
class NewChannelProposal(BaseModel):
|
|
||||||
channel: str
|
|
||||||
priority: str
|
|
||||||
rationale: str
|
|
||||||
|
|
||||||
|
|
||||||
class TransformationProposal(BaseModel):
|
|
||||||
brand_identity: list[AsIsToBeItem]
|
|
||||||
content_strategy: list[AsIsToBeItem]
|
|
||||||
platform_strategies: list[PlatformStrategy]
|
|
||||||
website_improvements: list[AsIsToBeItem]
|
|
||||||
new_channel_proposals: list[NewChannelProposal]
|
|
||||||
|
|
||||||
|
|
||||||
# --- Roadmap / KPI ---
|
|
||||||
|
|
||||||
class RoadmapTask(BaseModel):
|
|
||||||
task: str
|
|
||||||
completed: bool
|
|
||||||
|
|
||||||
|
|
||||||
class RoadmapMonth(BaseModel):
|
|
||||||
month: int
|
|
||||||
title: str
|
|
||||||
subtitle: str
|
|
||||||
tasks: list[RoadmapTask]
|
|
||||||
|
|
||||||
|
|
||||||
class KPIMetric(BaseModel):
|
|
||||||
metric: str
|
|
||||||
current: str
|
|
||||||
target_3_month: str
|
|
||||||
target_12_month: str
|
|
||||||
|
|
||||||
|
|
||||||
# --- ReportInput (prompt 템플릿 변수) ---
|
|
||||||
|
|
||||||
class ReportInput(BaseModel):
|
class ReportInput(BaseModel):
|
||||||
clinic_name: str | None = None
|
clinic_name: str | None = None
|
||||||
clinic_name_en: str | None = None
|
clinic_name_en: str | None = None
|
||||||
|
|
@ -319,25 +18,26 @@ class ReportInput(BaseModel):
|
||||||
gangnam_unni: str | None = None
|
gangnam_unni: str | None = None
|
||||||
|
|
||||||
|
|
||||||
# --- MarketingReport ---
|
class ChannelScore(BaseModel):
|
||||||
|
score: int
|
||||||
|
summary: str
|
||||||
|
strengths: list[str]
|
||||||
|
weaknesses: list[str]
|
||||||
|
|
||||||
class MarketingReport(BaseModel):
|
|
||||||
id: str
|
class ConversionStrategy(BaseModel):
|
||||||
created_at: str
|
summary: str
|
||||||
target_url: str
|
actions: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
# response_format으로 OpenAI structured output에 전달 — dict 필드 사용 불가
|
||||||
|
class ReportOutput(BaseModel):
|
||||||
overall_score: int
|
overall_score: int
|
||||||
clinic_snapshot: ClinicSnapshot
|
instagram: ChannelScore | None = None
|
||||||
channel_scores: list[ChannelScore]
|
facebook: ChannelScore | None = None
|
||||||
youtube_audit: YouTubeAudit
|
naver_blog: ChannelScore | None = None
|
||||||
instagram_audit: InstagramAudit
|
youtube: ChannelScore | None = None
|
||||||
facebook_audit: FacebookAudit
|
gangnam_unni: ChannelScore | None = None
|
||||||
other_channels: list[OtherChannel]
|
conversion_strategy: ConversionStrategy
|
||||||
website_audit: WebsiteAudit
|
roadmap: list[str]
|
||||||
problem_diagnosis: list[DiagnosisItem]
|
kpis: list[str]
|
||||||
transformation: TransformationProposal
|
|
||||||
roadmap: list[RoadmapMonth]
|
|
||||||
kpi_dashboard: list[KPIMetric]
|
|
||||||
screenshots: list[ScreenshotEvidence] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
ReportOutput = MarketingReport
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
from pydantic import BaseModel, ConfigDict
|
|
||||||
|
|
||||||
|
|
||||||
def _to_camel(s: str) -> str:
|
|
||||||
head, *tail = s.split("_")
|
|
||||||
return head + "".join(w.capitalize() for w in tail)
|
|
||||||
|
|
||||||
|
|
||||||
class CamelModel(BaseModel):
|
|
||||||
model_config = ConfigDict(populate_by_name=True, alias_generator=_to_camel)
|
|
||||||
|
|
@ -1,200 +1,16 @@
|
||||||
from typing import Literal
|
from pydantic import BaseModel
|
||||||
from models.common import CamelModel
|
|
||||||
|
|
||||||
|
|
||||||
class BrandPlanInconsistencyValue(CamelModel):
|
class PlanCreate(BaseModel):
|
||||||
channel: str
|
report_id: str
|
||||||
value: str
|
regenerate: bool = False
|
||||||
is_correct: bool
|
|
||||||
|
|
||||||
|
|
||||||
class BrandPlanInconsistency(CamelModel):
|
class PlanResponse(BaseModel):
|
||||||
field: str
|
|
||||||
values: list[BrandPlanInconsistencyValue]
|
|
||||||
impact: str
|
|
||||||
recommendation: str
|
|
||||||
|
|
||||||
|
|
||||||
# --- BrandGuide ---
|
|
||||||
|
|
||||||
class ColorSwatch(CamelModel):
|
|
||||||
name: str
|
|
||||||
hex: str
|
|
||||||
usage: str
|
|
||||||
|
|
||||||
|
|
||||||
class FontSpec(CamelModel):
|
|
||||||
family: str
|
|
||||||
weight: str
|
|
||||||
usage: str
|
|
||||||
sample_text: str
|
|
||||||
|
|
||||||
|
|
||||||
class LogoUsageRule(CamelModel):
|
|
||||||
rule: str
|
|
||||||
description: str
|
|
||||||
correct: bool
|
|
||||||
|
|
||||||
|
|
||||||
class ToneOfVoice(CamelModel):
|
|
||||||
personality: list[str]
|
|
||||||
communication_style: str
|
|
||||||
do_examples: list[str]
|
|
||||||
dont_examples: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelBrandingRule(CamelModel):
|
|
||||||
channel: str
|
|
||||||
icon: str
|
|
||||||
profile_photo: str
|
|
||||||
banner_spec: str
|
|
||||||
bio_template: str
|
|
||||||
current_status: Literal["correct", "incorrect", "missing"]
|
|
||||||
|
|
||||||
|
|
||||||
class BrandGuide(CamelModel):
|
|
||||||
colors: list[ColorSwatch]
|
|
||||||
fonts: list[FontSpec]
|
|
||||||
logo_rules: list[LogoUsageRule]
|
|
||||||
tone_of_voice: ToneOfVoice
|
|
||||||
channel_branding: list[ChannelBrandingRule]
|
|
||||||
brand_inconsistencies: list[BrandPlanInconsistency]
|
|
||||||
|
|
||||||
|
|
||||||
# --- ChannelStrategy ---
|
|
||||||
|
|
||||||
class ChannelStrategyCard(CamelModel):
|
|
||||||
channel_id: str
|
|
||||||
channel_name: str
|
|
||||||
icon: str
|
|
||||||
current_status: str
|
|
||||||
target_goal: str
|
|
||||||
content_types: list[str]
|
|
||||||
posting_frequency: str
|
|
||||||
tone: str
|
|
||||||
format_guidelines: list[str]
|
|
||||||
priority: Literal["P0", "P1", "P2"]
|
|
||||||
customer_journey_stage: Literal["awareness", "interest", "consideration", "conversion", "loyalty"] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# --- ContentStrategy ---
|
|
||||||
|
|
||||||
class ContentPillar(CamelModel):
|
|
||||||
title: str
|
|
||||||
description: str
|
|
||||||
related_usp: str
|
|
||||||
example_topics: list[str]
|
|
||||||
color: str
|
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeRow(CamelModel):
|
|
||||||
format: str
|
|
||||||
channels: list[str]
|
|
||||||
frequency: str
|
|
||||||
purpose: str
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowStep(CamelModel):
|
|
||||||
step: int
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
owner: str
|
|
||||||
duration: str
|
|
||||||
|
|
||||||
|
|
||||||
class RepurposingOutput(CamelModel):
|
|
||||||
format: str
|
|
||||||
channel: str
|
|
||||||
description: str
|
|
||||||
|
|
||||||
|
|
||||||
class ContentStrategyData(CamelModel):
|
|
||||||
pillars: list[ContentPillar]
|
|
||||||
type_matrix: list[ContentTypeRow]
|
|
||||||
workflow: list[WorkflowStep]
|
|
||||||
repurposing_source: str
|
|
||||||
repurposing_outputs: list[RepurposingOutput]
|
|
||||||
|
|
||||||
|
|
||||||
# --- Calendar ---
|
|
||||||
|
|
||||||
class CalendarEntry(CamelModel):
|
|
||||||
day_of_week: int
|
|
||||||
channel: str
|
|
||||||
channel_icon: str
|
|
||||||
content_type: Literal["video", "blog", "social", "ad"]
|
|
||||||
title: str
|
|
||||||
id: str | None = None
|
|
||||||
description: str | None = None
|
|
||||||
pillar: str | None = None
|
|
||||||
status: Literal["draft", "approved", "published"] | None = None
|
|
||||||
is_manual_edit: bool | None = None
|
|
||||||
ai_prompt_seed: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class CalendarWeek(CamelModel):
|
|
||||||
week_number: int
|
|
||||||
label: str
|
|
||||||
entries: list[CalendarEntry]
|
|
||||||
|
|
||||||
|
|
||||||
class ContentCountSummary(CamelModel):
|
|
||||||
type: Literal["video", "blog", "social", "ad"]
|
|
||||||
label: str
|
|
||||||
count: int
|
|
||||||
color: str
|
|
||||||
|
|
||||||
|
|
||||||
class CalendarData(CamelModel):
|
|
||||||
weeks: list[CalendarWeek]
|
|
||||||
monthly_summary: list[ContentCountSummary]
|
|
||||||
|
|
||||||
|
|
||||||
# --- AssetCollection ---
|
|
||||||
|
|
||||||
class AssetCard(CamelModel):
|
|
||||||
id: str
|
id: str
|
||||||
source: Literal["homepage", "naver_place", "blog", "social", "youtube"]
|
analysis_run_id: str
|
||||||
source_label: str
|
brand_guide: dict
|
||||||
type: Literal["photo", "video", "text"]
|
channel_strategies: list
|
||||||
title: str
|
content_strategy: dict
|
||||||
description: str
|
calendar: list
|
||||||
repurposing_suggestions: list[str]
|
|
||||||
status: Literal["collected", "pending", "needs_creation"]
|
|
||||||
|
|
||||||
|
|
||||||
class YouTubeRepurposeItem(CamelModel):
|
|
||||||
title: str
|
|
||||||
views: int
|
|
||||||
type: Literal["Short", "Long"]
|
|
||||||
repurpose_as: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
class AssetCollectionData(CamelModel):
|
|
||||||
assets: list[AssetCard]
|
|
||||||
youtube_repurpose: list[YouTubeRepurposeItem]
|
|
||||||
|
|
||||||
|
|
||||||
# --- Repurposing ---
|
|
||||||
|
|
||||||
class RepurposingProposalItem(CamelModel):
|
|
||||||
source_video: YouTubeRepurposeItem
|
|
||||||
outputs: list[RepurposingOutput]
|
|
||||||
estimated_effort: Literal["low", "medium", "high"]
|
|
||||||
priority: Literal["high", "medium", "low"]
|
|
||||||
|
|
||||||
|
|
||||||
# --- PlanApiResponse ---
|
|
||||||
|
|
||||||
class PlanApiResponse(CamelModel):
|
|
||||||
id: str
|
|
||||||
clinic_name: str | None = None
|
|
||||||
clinic_name_en: str | None = None
|
|
||||||
created_at: str
|
created_at: str
|
||||||
target_url: str
|
|
||||||
brand_guide: BrandGuide
|
|
||||||
channel_strategies: list[ChannelStrategyCard]
|
|
||||||
content_strategy: ContentStrategyData
|
|
||||||
calendar: CalendarData
|
|
||||||
asset_collection: AssetCollectionData
|
|
||||||
repurposing_proposals: list[RepurposingProposalItem] | None = None
|
|
||||||
|
|
|
||||||
|
|
@ -1,307 +1,22 @@
|
||||||
from __future__ import annotations
|
from pydantic import BaseModel
|
||||||
from models.common import CamelModel
|
|
||||||
from models.status import Severity, ChannelStatus, DataSource, Language, VideoType, AnnotationType
|
|
||||||
|
|
||||||
|
|
||||||
class ScreenshotAnnotation(CamelModel):
|
class ClinicInfo(BaseModel):
|
||||||
type: AnnotationType
|
name: str
|
||||||
x: float
|
url: str
|
||||||
y: float
|
|
||||||
width: float | None = None
|
|
||||||
height: float | None = None
|
|
||||||
label: str | None = None
|
|
||||||
color: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ScreenshotEvidence(CamelModel):
|
class ReportResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
url: str
|
clinic: ClinicInfo
|
||||||
channel: str
|
|
||||||
captured_at: str
|
|
||||||
caption: str
|
|
||||||
source_url: str | None = None
|
|
||||||
annotations: list[ScreenshotAnnotation] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class DiagnosisItem(CamelModel):
|
|
||||||
category: str
|
|
||||||
detail: str
|
|
||||||
severity: Severity
|
|
||||||
evidence_ids: list[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class LeadDoctor(CamelModel):
|
|
||||||
name: str
|
|
||||||
credentials: str
|
|
||||||
rating: float
|
|
||||||
review_count: int
|
|
||||||
|
|
||||||
|
|
||||||
class PriceRange(CamelModel):
|
|
||||||
min: str
|
|
||||||
max: str
|
|
||||||
currency: str
|
|
||||||
|
|
||||||
|
|
||||||
class LogoImages(CamelModel):
|
|
||||||
circle: str | None = None
|
|
||||||
horizontal: str | None = None
|
|
||||||
korean: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class BrandColors(CamelModel):
|
|
||||||
primary: str
|
|
||||||
accent: str
|
|
||||||
text: str
|
|
||||||
|
|
||||||
|
|
||||||
class RegistryData(CamelModel):
|
|
||||||
district: str | None = None
|
|
||||||
branches: str | None = None
|
|
||||||
brand_group: str | None = None
|
|
||||||
website_en: str | None = None
|
|
||||||
naver_place_url: str | None = None
|
|
||||||
gangnam_unni_url: str | None = None
|
|
||||||
google_maps_url: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ClinicSnapshot(CamelModel):
|
|
||||||
name: str
|
|
||||||
name_en: str
|
|
||||||
established: str
|
|
||||||
years_in_business: int
|
|
||||||
staff_count: int
|
|
||||||
lead_doctor: LeadDoctor
|
|
||||||
overall_rating: float
|
|
||||||
total_reviews: int
|
|
||||||
price_range: PriceRange
|
|
||||||
certifications: list[str]
|
|
||||||
media_appearances: list[str]
|
|
||||||
medical_tourism: list[str]
|
|
||||||
location: str
|
|
||||||
nearest_station: str
|
|
||||||
phone: str
|
|
||||||
domain: str
|
|
||||||
logo_images: LogoImages | None = None
|
|
||||||
brand_colors: BrandColors | None = None
|
|
||||||
source: DataSource | None = None
|
|
||||||
registry_data: RegistryData | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelScore(CamelModel):
|
|
||||||
channel: str
|
|
||||||
icon: str
|
|
||||||
score: int
|
|
||||||
max_score: int
|
|
||||||
status: Severity
|
|
||||||
headline: str
|
|
||||||
|
|
||||||
|
|
||||||
class WeeklyViewGrowth(CamelModel):
|
|
||||||
absolute: int
|
|
||||||
percentage: float
|
|
||||||
|
|
||||||
|
|
||||||
class EstimatedRevenue(CamelModel):
|
|
||||||
min: int
|
|
||||||
max: int
|
|
||||||
|
|
||||||
|
|
||||||
class LinkedUrl(CamelModel):
|
|
||||||
label: str
|
|
||||||
url: str
|
|
||||||
|
|
||||||
|
|
||||||
class TopVideo(CamelModel):
|
|
||||||
title: str
|
|
||||||
views: int
|
|
||||||
uploaded_ago: str
|
|
||||||
type: VideoType
|
|
||||||
duration: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class YouTubeAudit(CamelModel):
|
|
||||||
channel_name: str
|
|
||||||
handle: str
|
|
||||||
subscribers: int
|
|
||||||
total_videos: int
|
|
||||||
total_views: int
|
|
||||||
weekly_view_growth: WeeklyViewGrowth
|
|
||||||
estimated_monthly_revenue: EstimatedRevenue
|
|
||||||
avg_video_length: str
|
|
||||||
upload_frequency: str
|
|
||||||
channel_created_date: str
|
|
||||||
subscriber_rank: str
|
|
||||||
channel_description: str
|
|
||||||
linked_urls: list[LinkedUrl]
|
|
||||||
playlists: list[str]
|
|
||||||
top_videos: list[TopVideo]
|
|
||||||
diagnosis: list[DiagnosisItem]
|
|
||||||
|
|
||||||
|
|
||||||
class InstagramAccount(CamelModel):
|
|
||||||
handle: str
|
|
||||||
language: Language
|
|
||||||
label: str
|
|
||||||
posts: int
|
|
||||||
followers: int
|
|
||||||
following: int
|
|
||||||
category: str
|
|
||||||
profile_link: str
|
|
||||||
highlights: list[str]
|
|
||||||
reels_count: int
|
|
||||||
content_format: str
|
|
||||||
profile_photo: str
|
|
||||||
bio: str
|
|
||||||
|
|
||||||
|
|
||||||
class InstagramAudit(CamelModel):
|
|
||||||
accounts: list[InstagramAccount]
|
|
||||||
diagnosis: list[DiagnosisItem]
|
|
||||||
|
|
||||||
|
|
||||||
class BrandInconsistencyValue(CamelModel):
|
|
||||||
channel: str
|
|
||||||
value: str
|
|
||||||
is_correct: bool
|
|
||||||
|
|
||||||
|
|
||||||
class BrandInconsistency(CamelModel):
|
|
||||||
field: str
|
|
||||||
values: list[BrandInconsistencyValue]
|
|
||||||
impact: str
|
|
||||||
recommendation: str
|
|
||||||
|
|
||||||
|
|
||||||
class FacebookPage(CamelModel):
|
|
||||||
url: str
|
|
||||||
page_name: str
|
|
||||||
language: Language
|
|
||||||
label: str
|
|
||||||
followers: int
|
|
||||||
following: int
|
|
||||||
category: str
|
|
||||||
bio: str
|
|
||||||
logo: str
|
|
||||||
logo_description: str
|
|
||||||
link: str
|
|
||||||
linked_domain: str
|
|
||||||
reviews: int
|
|
||||||
recent_post_age: str
|
|
||||||
has_whatsapp: bool
|
|
||||||
post_frequency: str | None = None
|
|
||||||
top_content_type: str | None = None
|
|
||||||
engagement: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class FacebookAudit(CamelModel):
|
|
||||||
pages: list[FacebookPage]
|
|
||||||
diagnosis: list[DiagnosisItem]
|
|
||||||
brand_inconsistencies: list[BrandInconsistency]
|
|
||||||
consolidation_recommendation: str
|
|
||||||
|
|
||||||
|
|
||||||
class OtherChannel(CamelModel):
|
|
||||||
name: str
|
|
||||||
status: ChannelStatus
|
|
||||||
details: str
|
|
||||||
url: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TrackingPixel(CamelModel):
|
|
||||||
name: str
|
|
||||||
installed: bool
|
|
||||||
details: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class SnsLink(CamelModel):
|
|
||||||
platform: str
|
|
||||||
url: str
|
|
||||||
location: str
|
|
||||||
|
|
||||||
|
|
||||||
class AdditionalDomain(CamelModel):
|
|
||||||
domain: str
|
|
||||||
purpose: str
|
|
||||||
|
|
||||||
|
|
||||||
class WebsiteAudit(CamelModel):
|
|
||||||
primary_domain: str
|
|
||||||
additional_domains: list[AdditionalDomain]
|
|
||||||
sns_links_on_site: bool
|
|
||||||
sns_links_detail: list[SnsLink] | None = None
|
|
||||||
tracking_pixels: list[TrackingPixel]
|
|
||||||
main_cta: str
|
|
||||||
|
|
||||||
|
|
||||||
class AsIsToBeItem(CamelModel):
|
|
||||||
area: str
|
|
||||||
as_is: str
|
|
||||||
to_be: str
|
|
||||||
|
|
||||||
|
|
||||||
class StrategyDetail(CamelModel):
|
|
||||||
strategy: str
|
|
||||||
detail: str
|
|
||||||
|
|
||||||
|
|
||||||
class PlatformStrategy(CamelModel):
|
|
||||||
platform: str
|
|
||||||
icon: str
|
|
||||||
current_metric: str
|
|
||||||
target_metric: str
|
|
||||||
strategies: list[StrategyDetail]
|
|
||||||
|
|
||||||
|
|
||||||
class NewChannelProposal(CamelModel):
|
|
||||||
channel: str
|
|
||||||
priority: str
|
|
||||||
rationale: str
|
|
||||||
|
|
||||||
|
|
||||||
class TransformationProposal(CamelModel):
|
|
||||||
brand_identity: list[AsIsToBeItem]
|
|
||||||
content_strategy: list[AsIsToBeItem]
|
|
||||||
platform_strategies: list[PlatformStrategy]
|
|
||||||
website_improvements: list[AsIsToBeItem]
|
|
||||||
new_channel_proposals: list[NewChannelProposal]
|
|
||||||
|
|
||||||
|
|
||||||
class RoadmapTask(CamelModel):
|
|
||||||
task: str
|
|
||||||
completed: bool
|
|
||||||
|
|
||||||
|
|
||||||
class RoadmapMonth(CamelModel):
|
|
||||||
month: int
|
|
||||||
title: str
|
|
||||||
subtitle: str
|
|
||||||
tasks: list[RoadmapTask]
|
|
||||||
|
|
||||||
|
|
||||||
class KPIMetric(CamelModel):
|
|
||||||
metric: str
|
|
||||||
current: str
|
|
||||||
target_3_month: str
|
|
||||||
target_12_month: str
|
|
||||||
|
|
||||||
|
|
||||||
class MarketingReportResponse(CamelModel):
|
|
||||||
id: str
|
|
||||||
clinic_name: str | None = None
|
|
||||||
clinic_name_en: str | None = None
|
|
||||||
created_at: str
|
|
||||||
target_url: str
|
|
||||||
overall_score: int
|
overall_score: int
|
||||||
clinic_snapshot: ClinicSnapshot
|
youtube: dict
|
||||||
channel_scores: list[ChannelScore]
|
instagram: dict
|
||||||
youtube_audit: YouTubeAudit
|
facebook: dict
|
||||||
instagram_audit: InstagramAudit
|
naver_place: dict
|
||||||
facebook_audit: FacebookAudit
|
naver_blog: dict
|
||||||
other_channels: list[OtherChannel]
|
gangnam_unni: dict
|
||||||
website_audit: WebsiteAudit
|
conversion_strategy: dict
|
||||||
problem_diagnosis: list[DiagnosisItem]
|
roadmap: list
|
||||||
transformation: TransformationProposal
|
kpis: list
|
||||||
roadmap: list[RoadmapMonth]
|
generated_at: str
|
||||||
kpi_dashboard: list[KPIMetric]
|
|
||||||
screenshots: list[ScreenshotEvidence] | None = None
|
|
||||||
|
|
|
||||||
|
|
@ -14,39 +14,3 @@ class TaskStatus(StrEnum):
|
||||||
START = "start"
|
START = "start"
|
||||||
PROCESSING = "processing"
|
PROCESSING = "processing"
|
||||||
DONE = "done"
|
DONE = "done"
|
||||||
|
|
||||||
|
|
||||||
class Severity(StrEnum):
|
|
||||||
CRITICAL = "critical"
|
|
||||||
WARNING = "warning"
|
|
||||||
GOOD = "good"
|
|
||||||
EXCELLENT = "excellent"
|
|
||||||
UNKNOWN = "unknown"
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelStatus(StrEnum):
|
|
||||||
ACTIVE = "active"
|
|
||||||
INACTIVE = "inactive"
|
|
||||||
UNKNOWN = "unknown"
|
|
||||||
NOT_FOUND = "not_found"
|
|
||||||
|
|
||||||
|
|
||||||
class DataSource(StrEnum):
|
|
||||||
REGISTRY = "registry"
|
|
||||||
SCRAPE = "scrape"
|
|
||||||
|
|
||||||
|
|
||||||
class Language(StrEnum):
|
|
||||||
KR = "KR"
|
|
||||||
EN = "EN"
|
|
||||||
|
|
||||||
|
|
||||||
class VideoType(StrEnum):
|
|
||||||
SHORT = "Short"
|
|
||||||
LONG = "Long"
|
|
||||||
|
|
||||||
|
|
||||||
class AnnotationType(StrEnum):
|
|
||||||
HIGHLIGHT = "highlight"
|
|
||||||
ARROW = "arrow"
|
|
||||||
TEXT = "text"
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue