틱톡·영문 인스타/페북 채널 수집 추가
- apify: 틱톡 프로필 액터 - mock_urls.py: 클리닉별 채널 URL 매핑 (mockUrls.json → 파이썬 모듈) - api/analysis: homepage 매칭으로 미지원 채널 보충 (추후 DB) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>insta-data
parent
cb798f7acc
commit
9817b53be1
|
|
@ -8,10 +8,28 @@ from models.file import FileListItem, FileType, FileUploadResponse
|
|||
from models.status import AnalysisStatus
|
||||
from services.pipeline import run_pipeline
|
||||
from services.file import get_analysis_files_response, handle_analysis_file_upload, soft_delete_analysis_file
|
||||
from mock_urls import MOCK_CLINICS
|
||||
from common.utils import _normalize_homepage, _with_scheme
|
||||
|
||||
router = APIRouter(prefix="/api/analysis", tags=["analysis"], dependencies=[Depends(verify_api_key)])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 추후 DB에 클리닉별로 매핑할 채널(틱톡/영문 인스타·페북). 지금은 mock_urls에서 homepage 매칭으로 보충.
|
||||
def _extra_channels_from_mockurls(homepage_url: str) -> dict:
|
||||
"""homepage로 mock_urls에서 클리닉을 찾아 틱톡/영문 인스타·페북 URL 반환 (없으면 {})."""
|
||||
target = _normalize_homepage(homepage_url)
|
||||
if not target:
|
||||
return {}
|
||||
for c in MOCK_CLINICS:
|
||||
urls = c["urls"]
|
||||
if _normalize_homepage(urls.get("homepage", "")) == target:
|
||||
return {
|
||||
"tiktok": _with_scheme(urls.get("tiktok")),
|
||||
"instagram_en": _with_scheme(urls.get("instagramEn")),
|
||||
"facebook_en": _with_scheme(urls.get("facebookEn")),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_202_ACCEPTED, response_model=AnalysisStartResponse)
|
||||
async def start_analysis(body: AnalysisCreate, background_tasks: BackgroundTasks):
|
||||
|
|
@ -38,7 +56,15 @@ async def start_analysis(body: AnalysisCreate, background_tasks: BackgroundTasks
|
|||
ig_id, fb_id, nb_id, yt_id, gu_id,
|
||||
)
|
||||
|
||||
background_tasks.add_task(run_pipeline, analysis_run_id)
|
||||
# 클라 값 우선, 없으면 보충 (추후 DB에서 클리닉별로 가져올 값)
|
||||
mock_extra = _extra_channels_from_mockurls(hospital["url"])
|
||||
extra_channels = {
|
||||
"tiktok": body.channels.tiktok or mock_extra.get("tiktok"),
|
||||
"instagram_en": body.channels.instagram_en or mock_extra.get("instagram_en"),
|
||||
"facebook_en": body.channels.facebook_en or mock_extra.get("facebook_en"),
|
||||
}
|
||||
logger.info("[analysis] extra_channels=%s (mock_matched=%s)", extra_channels, bool(mock_extra))
|
||||
background_tasks.add_task(run_pipeline, analysis_run_id, extra_channels)
|
||||
|
||||
return AnalysisStartResponse(
|
||||
analysis_run_id=analysis_run_id,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class ApifyClient:
|
|||
return None
|
||||
return {
|
||||
"username": profile["username"],
|
||||
"profileImage": profile.get("profilePicUrlHD") or profile.get("profilePicUrl"),
|
||||
"followers": profile.get("followersCount", 0),
|
||||
"following": profile.get("followsCount", 0),
|
||||
"posts": profile.get("postsCount", 0),
|
||||
|
|
@ -134,6 +135,7 @@ class ApifyClient:
|
|||
return None
|
||||
return {
|
||||
"pageName": page.get("title") or page.get("name"),
|
||||
"profileImage": page.get("profilePictureUrl") or page.get("profilePhoto") or page.get("profilePic"),
|
||||
"pageUrl": page.get("pageUrl", page_url),
|
||||
"followers": page.get("followers", 0),
|
||||
"likes": page.get("likes", 0),
|
||||
|
|
@ -145,3 +147,45 @@ class ApifyClient:
|
|||
"intro": page.get("intro"),
|
||||
"rating": page.get("rating"),
|
||||
}
|
||||
|
||||
async def fetch_tiktok_profile(self, url: str) -> list[dict]:
|
||||
user = urlparse(url).path.strip("/").lstrip("@").split("/")[0] if "://" in url else url.lstrip("@")
|
||||
return await self._run_actor("clockworks~tiktok-scraper", {
|
||||
"profiles": [user],
|
||||
"resultsPerPage": 10,
|
||||
"profileScrapeSections": ["videos"],
|
||||
"profileSorting": "latest",
|
||||
"shouldDownloadVideos": False,
|
||||
"shouldDownloadCovers": False,
|
||||
"shouldDownloadSubtitles": False,
|
||||
})
|
||||
|
||||
async def get_tiktok_profile(self, url: str) -> dict | None:
|
||||
items = await self.fetch_tiktok_profile(url)
|
||||
if not items:
|
||||
return None
|
||||
author = (items[0] or {}).get("authorMeta") or {}
|
||||
videos = [
|
||||
{
|
||||
"title": (v.get("text") or "")[:300],
|
||||
"playCount": v.get("playCount", 0),
|
||||
"diggCount": v.get("diggCount", 0),
|
||||
"commentCount": v.get("commentCount", 0),
|
||||
"shareCount": v.get("shareCount", 0),
|
||||
"createTime": v.get("createTimeISO"),
|
||||
"url": v.get("webVideoUrl"),
|
||||
}
|
||||
for v in items if isinstance(v, dict)
|
||||
]
|
||||
return {
|
||||
"handle": author.get("name"),
|
||||
"profileImage": author.get("avatar"),
|
||||
"nickname": author.get("nickName"),
|
||||
"followers": author.get("fans", 0),
|
||||
"following": author.get("following", 0),
|
||||
"likes": author.get("heart", 0),
|
||||
"videoCount": author.get("video", 0),
|
||||
"verified": author.get("verified", False),
|
||||
"bio": author.get("signature", ""),
|
||||
"recentVideos": videos[:10],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
# 프론트가 아직 안 보내는 채널(틱톡/영문 인스타·페북)을 homepage로 매칭해 보충하는 임시 mock 데이터.
|
||||
# 기존 mockUrls.json을 파이썬 모듈로 전환 — 런타임 파일 I/O 없이 직접 import.
|
||||
|
||||
MOCK_CLINICS = [
|
||||
{
|
||||
"label": "뷰성형외과",
|
||||
"urls": {
|
||||
"homepage": "viewclinic.com",
|
||||
"youtube": "youtube.com/channel/UCQqqH3Klj2HQSHNNSVug-CQ",
|
||||
"instagram": "instagram.com/viewplastic",
|
||||
"facebook": "facebook.com/viewps1",
|
||||
"naverPlace": "https://naver.me/x9BxGXkK",
|
||||
"naverBlog": "blog.naver.com/viewclinicps",
|
||||
"gangnamUnni": "gangnamunni.com/hospitals/189",
|
||||
"tiktok": "tiktok.com/@viewplastic",
|
||||
"tiktokEn": "tiktok.com/@viewplasticsurgery",
|
||||
"instagramEn": "instagram.com/view_plastic_surgery",
|
||||
"facebookEn": "facebook.com/viewclinic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "바노바기 성형외과",
|
||||
"urls": {
|
||||
"homepage": "banobagi.com",
|
||||
"youtube": "youtube.com/c/banobagips",
|
||||
"instagram": "instagram.com/banobagi_ps",
|
||||
"facebook": "facebook.com/BanobagiPlasticSurgery",
|
||||
"naverPlace": "https://naver.me/xxY2yLr5",
|
||||
"naverBlog": "blog.naver.com/banobagiprs",
|
||||
"gangnamUnni": "gangnamunni.com/hospitals/23",
|
||||
"tiktok": "",
|
||||
"instagramEn": "instagram.com/english_banobagi",
|
||||
"facebookEn": "facebook.com/englishbanobagi"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "ID 성형외과",
|
||||
"urls": {
|
||||
"homepage": "idhospital.com",
|
||||
"youtube": "youtube.com/user/IDhospital",
|
||||
"instagram": "instagram.com/idhospital",
|
||||
"facebook": "facebook.com/idhospital0050",
|
||||
"naverPlace": "https://naver.me/GtURpCEn",
|
||||
"naverBlog": "",
|
||||
"gangnamUnni": "gangnamunni.com/hospitals/257",
|
||||
"tiktok": "tiktok.com/@idhospitalkorea",
|
||||
"instagramEn": "instagram.com/idhospitalkorea",
|
||||
"facebookEn": "facebook.com/idhospital.eng"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "JK 성형외과",
|
||||
"urls": {
|
||||
"homepage": "jkplastic.com",
|
||||
"youtube": "youtube.com/channel/UC5F8dEt32hdp3cTeFyls4qg",
|
||||
"instagram": "instagram.com/jkplasticsurgery_kr",
|
||||
"facebook": "facebook.com/jkmedicalgroup",
|
||||
"naverPlace": "https://naver.me/x67y6cAc",
|
||||
"naverBlog": "blog.naver.com/jkstory1",
|
||||
"gangnamUnni": "gangnamunni.com/hospitals/858",
|
||||
"tiktok": "tiktok.com/@jkplastic",
|
||||
"instagramEn": "instagram.com/jkplasticsurgery",
|
||||
"facebookEn": "facebook.com/jkplastic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "그랜드 성형외과",
|
||||
"urls": {
|
||||
"homepage": "grandsurgery.com",
|
||||
"youtube": "youtube.com/channel/UCU2o_aHqsNFuqwtdzVM3xbQ",
|
||||
"instagram": "instagram.com/grand_korea",
|
||||
"facebook": "facebook.com/grandps.korea",
|
||||
"naverPlace": "https://naver.me/Fw7MYKWK",
|
||||
"naverBlog": "blog.naver.com/grandprs",
|
||||
"gangnamUnni": "gangnamunni.com/hospitals/62",
|
||||
"tiktok": "",
|
||||
"instagramEn": "instagram.com/grandps_eng",
|
||||
"facebookEn": "facebook.com/grandplasticsurgery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "BK 성형외과",
|
||||
"urls": {
|
||||
"homepage": "bkhospital.com",
|
||||
"youtube": "youtube.com/channel/UChJONft3hemy5DGbXUveTFg",
|
||||
"instagram": "instagram.com/bkhospital_korea",
|
||||
"facebook": "",
|
||||
"naverPlace": "https://naver.me/517CTH3W",
|
||||
"naverBlog": "",
|
||||
"gangnamUnni": "",
|
||||
"tiktok": "",
|
||||
"instagramEn": "instagram.com/english_bkhospital",
|
||||
"facebookEn": "facebook.com/BKPSKoreaE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "톡스앤필",
|
||||
"urls": {
|
||||
"homepage": "toxnfill.com",
|
||||
"youtube": "youtube.com/channel/UCFpFZkm7mclD-z_-j7FTUag",
|
||||
"instagram": "instagram.com/toxnfill_official",
|
||||
"facebook": "facebook.com/toxnfill.official",
|
||||
"naverPlace": "https://naver.me/FvEmJIHA",
|
||||
"naverBlog": "blog.naver.com/toxnfill",
|
||||
"gangnamUnni": "gangnamunni.com/hospitals/3702",
|
||||
"tiktok": "tiktok.com/@toxnfillglobal",
|
||||
"instagramEn": "instagram.com/toxnfill_global",
|
||||
"facebookEn": "facebook.com/p/Toxnfill-Global-61557593068252"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "더 압구정 성형외과",
|
||||
"urls": {
|
||||
"homepage": "theclinic.co.kr",
|
||||
"youtube": "youtube.com/user/theplasticsurgery1",
|
||||
"instagram": "instagram.com/the_plasticsurgery",
|
||||
"facebook": "facebook.com/THEPS16445998",
|
||||
"naverPlace": "",
|
||||
"naverBlog": "blog.naver.com/with_theps",
|
||||
"gangnamUnni": "gangnamunni.com/hospitals/30",
|
||||
"tiktok": "",
|
||||
"instagramEn": "instagram.com/the_plasticsurgery.en",
|
||||
"facebookEn": "facebook.com/theps.english"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "오라클 성형외과",
|
||||
"urls": {
|
||||
"homepage": "oracleclinic.com",
|
||||
"youtube": "youtube.com/@oracle_medical_group",
|
||||
"instagram": "instagram.com/oraclemedicalgroup",
|
||||
"facebook": "facebook.com/oracleclinickr",
|
||||
"naverPlace": "https://naver.me/GhbU3VtK",
|
||||
"naverBlog": "",
|
||||
"gangnamUnni": "gangnamunni.com/hospitals/125",
|
||||
"tiktok": "tiktok.com/@oracleclinic_usa",
|
||||
"instagramEn": "instagram.com/oracleclinic_global",
|
||||
"facebookEn": "facebook.com/oracleclinicglobal"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -8,6 +8,9 @@ class Channels(BaseModel):
|
|||
facebook: str | None = None
|
||||
naver_blog: str | None = None
|
||||
gangnam_unni: str | None = None
|
||||
tiktok: str | None = None
|
||||
instagram_en: str | None = None
|
||||
facebook_en: str | None = None
|
||||
|
||||
|
||||
class AnalysisOptions(BaseModel):
|
||||
|
|
|
|||
Loading…
Reference in New Issue