diff --git a/data/clinic_social_registry.json b/data/clinic_social_registry.json new file mode 100644 index 0000000..bf21c81 --- /dev/null +++ b/data/clinic_social_registry.json @@ -0,0 +1,882 @@ +{ + "바노바기성형외과": { + "facebook": [ + "https://facebook.com/BanobagiPlasticSurgery?_rdr", + "https://www.facebook.com/BanobagiPlasticSurgery" + ], + "blog": [ + "https://blog.naver.com/banobagips", + "https://blog.naver.com/banobagiprs" + ], + "youtube": [ + "https://www.youtube.com/c/banobagips" + ], + "instagram": [ + "https://www.instagram.com/banobagi15886508", + "https://www.instagram.com/banobagi_ps/" + ] + }, + "뷰성형외과": { + "facebook": [ + "https://www.facebook.com/viewps1/" + ], + "blog": [ + "https://blog.naver.com/viewclinicps" + ], + "youtube": [ + "https://www.youtube.com/channel/UCQqqH3Klj2HQSHNNSVug-CQ", + "https://www.youtube.com/@ViewclinicKR/shorts" + ], + "instagram": [ + "https://www.instagram.com/viewplastic/" + ], + "cafe": [ + "https://cafe.naver.com/bluectcom2" + ] + }, + "아이디병원": { + "facebook": [ + "https://www.facebook.com/idhospital0050" + ], + "youtube": [ + "https://www.youtube.com/player_api?enablejsapi=1", + "https://www.youtube.com/user/IDhospital/featured" + ], + "instagram": [ + "https://www.instagram.com/idhospital" + ] + }, + "그랜드성형외과": { + "facebook": [ + "https://www.facebook.com/grandps.korea", + "https://facebook.com/grandps.korea" + ], + "blog": [ + "https://blog.naver.com/grandprs" + ], + "youtube": [ + "https://www.youtube.com/channel/UCU2o_aHqsNFuqwtdzVM3xbQ" + ], + "instagram": [ + "https://www.instagram.com/grand_korea", + "https://www.instagram.com/grand_korea/" + ] + }, + "원진성형외과": { + "facebook": [ + "https://www.facebook.com/KwonjinPS" + ], + "blog": [ + "https://blog.naver.com/popokpop" + ], + "youtube": [ + "https://www.youtube.com/channel/UCNBaQBCok5xIViCnrGdisoQ", + "https://www.youtube.com/@wjwonjin", + "https://www.youtube.com/channel/UC-EBM4e0VST1W_PQ0NzFMQg" + ], + "instagram": [ + "https://www.instagram.com/wonjin_official/", + "https://www.instagram.com/wonjin_ps/", + "https://www.instagram.com/wj_cosmetic/" + ] + }, + "마인드성형외과": { + "youtube": [ + "https://www.youtube.com/channel/UCzM5tIgkC8Es10YmLI55R_w" + ] + }, + "브라운성형외과": { + "facebook": [ + "https://www.facebook.com/braunps" + ], + "youtube": [ + "https://youtu.be/rtDaps_Ztlo?si=21DTQ2g8IkgRo4AW", + "https://youtu.be/P7t_Q1s6FKw?si=zotjNYKOgxEPIv6k", + "https://youtu.be/1TzG7l3vJH0?si=nyxUQhNle_xEh3nP", + "https://www.youtube.com/@10ricebone_braun", + "https://youtu.be/kZP1TxOSszQ?si=QYzJ-o7f9djliZ-8", + "https://youtu.be/nUeyQVTkA-A?si=dgbOKXBCJo7fhD2T", + "https://youtu.be/gRb-Pj0K4Nk?si=zcblxytyL7vLBxKL", + "https://youtu.be/JAlQhrvfd6E?si=ALygwB4qmWLpYFKp", + "https://youtu.be/9TGup2iLGXo?si=-ORV8WN0QpWERwO3", + "https://youtu.be/gA-ZEWoZQQk?si=y7icwpFlaFadEp_R", + "https://youtu.be/Z--rM9HhOcg?si=kad21Sd0KDgkLj10", + "https://www.youtube.com/@braunps_kr" + ], + "instagram": [ + "https://www.instagram.com/braunps_official/" + ] + }, + "오메가성형외과": { + "facebook": [ + "https://www.facebook.com/%EC%98%A4%EB%A9%94%EA%B0%80%EC%84%B1%ED%98%95%EC%99%B8%EA%B3%BC-1682577712065719/" + ] + }, + "나나성형외과": { + "facebook": [ + "https://www.facebook.com/nanaprs2", + "https://www.facebook.com/profile.php?id=100083037025459" + ], + "blog": [ + "https://blog.naver.com/nanaprs1" + ], + "youtube": [ + "https://youtube.com/@Nanaprstv?si=fLyqXsfJcLEhTmcH", + "https://www.youtube.com/나나성형외과", + "https://youtu.be/:id", + "https://www.youtube.com/@Nanaprstv" + ], + "instagram": [ + "https://www.instagram.com/nana_afters/", + "https://www.instagram.com/nanaprs", + "https://www.instagram.com/nanaprs/" + ], + "cafe": [ + "http://cafe.naver.com/bestcarbon" + ] + }, + "노트성형외과": { + "facebook": [ + "https://www.facebook.com/noteprs2/?eid=ARChdUaMnfPx5Xok2tDTXWr5IbgeQIlbLHKhDP2AfBXBPvRAY8mwZlVG4JdxderLMdBNRcVqXVVbaM4P" + ], + "youtube": [ + "https://youtu.be/I06kpsK0IZI", + "https://youtu.be/a7tU62mZvGE", + "https://youtu.be/-Y_UT3G5WoA", + "https://youtu.be/mAgui9wOowA", + "https://youtu.be/o-PBXufp9oA", + "https://youtu.be/9b4MWwxhVgE", + "https://youtu.be/Mev2OvcDvO4", + "https://youtu.be/HZjQJ-rcjoM", + "https://youtu.be/avQXLEoR7Gg", + "https://youtu.be/7wB3nCoxd-E", + "https://youtu.be/WpiKrNE68ns", + "https://youtu.be/ggCQD3eD_pc", + "https://youtu.be/lRHge9NFxWE", + "https://youtu.be/cZF4BRyIABM", + "https://youtu.be/Z0_lrBarjWA" + ], + "instagram": [ + "https://www.instagram.com/note.prs/?hl=ko" + ] + }, + "디에이성형외과": { + "youtube": [ + "https://www.youtube.com/channel/UC0wlA-w5JIt0G0EeWQf65AQ" + ], + "instagram": [ + "https://www.instagram.com/da_plastic_surgery" + ] + }, + "에이비성형외과": {}, + "비아이오성형외과": { + "facebook": [ + "https://www.facebook.com/biopskorea" + ], + "blog": [ + "https://blog.naver.com/bioplastics20" + ], + "instagram": [ + "https://www.instagram.com/bio_dr.hongsungpyo" + ] + }, + "제이준성형외과": { + "facebook": [ + "https://www.facebook.com/jayjunps" + ], + "blog": [ + "https://blog.naver.com/gusrlf00" + ], + "youtube": [ + "https://www.youtube.com/channel/UC1p4msJnetDKeOW-B4nlwZA/videos" + ], + "instagram": [ + "https://www.instagram.com/jayjunps/" + ] + }, + "마블성형외과": { + "facebook": [ + "https://www.facebook.com/hashtag/", + "https://www.facebook.com/marbleps/" + ], + "blog": [ + "https://blog.naver.com/marbleplastic" + ], + "youtube": [ + "https://www.youtube.com/@marbleps" + ], + "instagram": [ + "https://www.instagram.com/marble__ps/" + ] + }, + "쥬얼리성형외과": { + "facebook": [ + "https://www.facebook.com/juelyps" + ], + "blog": [ + "https://blog.naver.com/prsshin2" + ], + "youtube": [ + "http://www.youtube.com/jewelrypsKr1", + "https://www.youtube.com/JewelrypsKr1/videos" + ], + "instagram": [ + "https://instagram.com/jewelryps_kr" + ] + }, + "티에스성형외과": { + "facebook": [ + "https://www.facebook.com/tsprs" + ], + "blog": [ + "https://blog.naver.com/tsprs" + ], + "youtube": [ + "https://www.youtube.com/channel/UCgnizu8p7lbCnfIs76O8J-A" + ], + "instagram": [ + "https://www.instagram.com/tsprs_official/" + ], + "cafe": [ + "https://cafe.naver.com/yeuntalk" + ] + }, + "유노성형외과": { + "facebook": [ + "https://www.facebook.com/doctor.yuno/" + ], + "blog": [ + "https://blog.naver.com/yuno_blog" + ], + "youtube": [ + "https://www.youtube.com/@yunotube_yunoprs", + "https://www.youtube.com/channel/UCrEe-LfBLXA1-bTGS4bWmtg/featured" + ], + "instagram": [ + "https://www.instagram.com/doctor.yuno/" + ] + }, + "리팅성형외과": { + "blog": [ + "https://blog.naver.com/night140160" + ], + "youtube": [ + "https://www.youtube.com/channel/UC1xWD4jMLNtRjrXPtr468mA" + ], + "instagram": [ + "https://www.instagram.com/liting_psps/" + ] + }, + "앤써성형외과": { + "facebook": [ + "https://www.facebook.com/answerps4881", + "https://www.facebook.com/answerps4881/?ref=bookmarks" + ], + "blog": [ + "https://blog.naver.com/answerps" + ], + "youtube": [ + "https://www.youtube.com/channel/UCo7efud2JIGvGjKSkMpdQbA", + "https://www.youtube.com/@TV-gf8ms/featured" + ], + "instagram": [ + "https://www.instagram.com/answerps", + "https://www.instagram.com/answerps/" + ], + "cafe": [ + "https://cafe.naver.com/answerps" + ] + }, + "더픽스성형외과": { + "youtube": [ + "https://youtu.be/1xSyeNHkBRg?si=BXdIg5YLP0_5rSAx" + ], + "instagram": [ + "https://www.instagram.com/thefixps/" + ], + "cafe": [ + "https://cafe.naver.com/bluegrayewefn" + ] + }, + "기린성형외과": { + "blog": [ + "https://blog.naver.com/girinlife" + ], + "youtube": [ + "https://www.youtube.com/@girin_official", + "https://www.youtube.com/@girin_official/videos", + "https://www.youtube.com/@girin_official/shorts" + ], + "instagram": [ + "https://www.instagram.com/girin_ps_official/" + ] + }, + "압구정서울성형외과": { + "youtube": [ + "https://www.youtube.com/player_api", + "https://www.youtube.com/channel/UCqNtVCL2u5Xvx74ymboxDyg", + "https://www.youtube.com/@aspsno1", + "https://youtu.be/WENjXjsyBTY?t=738", + "https://youtu.be/9Z0anr2o0SI" + ] + }, + "리젠메디컬그룹": { + "blog": [ + "https://blog.naver.com/shinygn1", + "https://blog.naver.com/shinygn1/221607462891" + ], + "instagram": [ + "http://instagram.com/regenskin" + ] + }, + "코코성형외과": { + "facebook": [ + "https://www.facebook.com/kokodoctor/" + ], + "blog": [ + "https://blog.naver.com/koko_plastic_surgery" + ], + "youtube": [ + "https://www.youtube.com/channel/UCc19LE6Elp0OTNo9gDizJMw", + "https://www.youtube.com/channel/UCtoEbWkvprRpGfTtgLf-cIQ", + "https://www.youtube.com/channel/UCvGhy-kpyXlXR4SwtLh1q5w" + ], + "instagram": [ + "https://www.instagram.com/koko_ps_official/" + ] + }, + "오브제성형외과": { + "blog": [ + "https://blog.naver.com/objetps" + ], + "youtube": [ + "https://www.youtube.com/channel/UC2QrFhj-S8oUrbfOp1NXKZA?view_as=subscriber" + ] + }, + "리상성형외과": { + "blog": [ + "https://blog.naver.com/theps_kor" + ], + "youtube": [ + "https://www.youtube.com/@theregen_ps" + ], + "instagram": [ + "https://www.instagram.com/theregen_ps" + ] + }, + "에이트성형외과": { + "blog": [ + "https://blog.naver.com/eightplasticsurgery" + ], + "youtube": [ + "https://www.youtube.com/@eighteffect", + "https://www.youtube.com/channel/UCpTmm84yJLgIBwBcBhjie4Q" + ], + "instagram": [ + "https://www.instagram.com/eightps8", + "https://www.instagram.com/eightps8/" + ] + }, + "오페라성형외과": { + "facebook": [ + "https://www.facebook.com/operasurgery/" + ], + "blog": [ + "https://blog.naver.com/fasolt2" + ], + "youtube": [ + "http://youtu.be/DujfpXOKUp8?t=13m8s", + "https://www.youtube.com/channel/UC2R_jfmmn0zE-dkButFS_ZA" + ], + "instagram": [ + "https://www.instagram.com/opera_ps/" + ] + }, + "리엔장성형외과": { + "youtube": [ + "https://www.youtube.com/channel/UCxyiPTH9xHqhPVfwyyosXMQ" + ], + "instagram": [ + "https://www.instagram.com/lienjang_official/" + ] + }, + "루호성형외과": { + "instagram": [ + "https://www.instagram.com/dr.park1.luho/" + ] + }, + "쏘울성형외과": {}, + "에이탑성형외과": { + "blog": [ + "http://blog.naver.com/josoblue", + "https://blog.naver.com/aspect11706/" + ], + "youtube": [ + "https://www.youtube.com/channel/UCPBCTxCX1hyRnXejA8QfFEQ" + ], + "instagram": [ + "https://www.instagram.com/atop_instalog/" + ] + }, + "유캔비성형외과": { + "facebook": [ + "https://www.facebook.com/ucanbps" + ], + "blog": [ + "https://blog.naver.com/ucanb2338" + ], + "youtube": [ + "https://www.youtube.com/channel/UCegLA3CpLkDOe5h_DnhUpFA" + ], + "instagram": [ + "https://www.instagram.com/ucanb_plastic_surgery/" + ] + }, + "스타성형외과": { + "cafe": [ + "https://cafe.naver.com/starclinic", + "http://cafe.naver.com/starclinic" + ] + }, + "아이템성형외과": { + "facebook": [ + "https://www.facebook.com/%EC%9E%87%ED%85%9C%EC%84%B1%ED%98%95%EC%99%B8%EA%B3%BC-101634361973942" + ], + "blog": [ + "https://blog.naver.com/ittem_ps" + ], + "youtube": [ + "https://www.youtube.com/channel/UCXmZn2rvsl3S-tB901MjZxA" + ], + "instagram": [ + "https://www.instagram.com/ittem_ps" + ], + "cafe": [ + "https://cafe.naver.com/gkrry652", + "http://cafe.daum.net/plasticsurg" + ] + }, + "리코성형외과": { + "facebook": [ + "https://www.facebook.com/licotop" + ], + "blog": [ + "https://blog.naver.com/licops" + ], + "youtube": [ + "https://www.youtube.com/channel/UC3T_EiEpQr1xE16o69XCqjQ" + ] + }, + "플레저성형외과": { + "blog": [ + "https://blog.naver.com/2amsomething" + ], + "youtube": [ + "https://www.youtube.com/channel/UCAkB7Oj9cIG4gJNPsnFedRg", + "https://www.youtube.com/c/PSPS_korea" + ], + "instagram": [ + "https://www.instagram.com/pleasure_ps/" + ] + }, + "아이웰성형외과": {}, + "글로비성형외과": { + "facebook": [ + "https://www.facebook.com/glovips" + ], + "blog": [ + "https://blog.naver.com/glovips", + "https://blog.naver.com/tla92435" + ], + "youtube": [ + "https://youtu.be/wIZE-fQCFCs", + "https://www.youtube.com/channel/UCH4Orbmc3cFqajWx1I6Xs2w" + ], + "instagram": [ + "https://www.instagram.com/glovips/" + ] + }, + "엘르성형외과": {}, + "원픽성형외과": {}, + "탑페이스성형외과": { + "facebook": [ + "https://www.facebook.com/topfaceps", + "https://www.facebook.com/topfaceplasticsurgery", + "https://www.facebook.com/topfacepsthai" + ], + "blog": [ + "https://blog.naver.com/topfaceps" + ], + "youtube": [ + "https://www.youtube.com/@topfaceps" + ], + "instagram": [ + "https://www.instagram.com/topfaceps/", + "https://www.instagram.com/topfacepscn/", + "https://www.instagram.com/topfacejp/" + ], + "cafe": [ + "https://cafe.naver.com/gematsal" + ] + }, + "세민성형외과": {}, + "에이치성형외과": { + "facebook": [ + "https://www.facebook.com/3DFIT100" + ], + "blog": [ + "http://blog.naver.com/ys100ps" + ], + "youtube": [ + "https://www.youtube.com/channel/UC6d8i9WwIkm161ueFyVU2PA" + ] + }, + "리노보성형외과": {}, + "디엘성형외과": { + "facebook": [ + "https://www.facebook.com/dreamline.en", + "https://www.facebook.com/dl.plastic.en", + "https://www.facebook.com/DLplastic", + "https://www.facebook.com/DL.Plastic.TH", + "https://www.facebook.com/profile.php?id=61553776546703", + "https://www.facebook.com/DL.Plastic.VN" + ], + "blog": [ + "https://blog.naver.com/aesthetics182" + ], + "youtube": [ + "https://www.youtube.com/@LINEKING_DL", + "https://www.youtube.com/channel/UCpR9Qd_jBsouxinuJ5m-fvg" + ], + "instagram": [ + "https://www.instagram.com/dl_dreamline", + "https://www.instagram.com/dl_plastic_official", + "https://www.instagram.com/dl_dreamline.en", + "https://www.instagram.com/dl_plastic_int/", + "https://www.instagram.com/dl.plastic.jp?igsh=MXQ2ZW15ZWppY3BvaQ%3D%3D", + "https://www.instagram.com/dlplasty", + "https://www.instagram.com/dl_plastic_th", + "https://www.instagram.com/dl.plastic.surgery_vietnam?igsh=bGFmYWQ2MnJleXdy&utm_source=qr", + "https://www.instagram.com/dl_plastic_vn" + ] + }, + "피알성형외과": { + "facebook": [ + "https://www.facebook.com/PRPRS.official" + ], + "youtube": [ + "https://www.youtube.com/channel/UCFeYqxaNozoRaTbYOrR16lg" + ], + "instagram": [ + "https://api.instagram.com/v1/users/self/media/recent?client_id=", + "https://www.instagram.com/prprs_official/" + ] + }, + "라프린성형외과": {}, + "도도성형외과": { + "blog": [ + "http://blog.naver.com/mmscjh" + ] + }, + "엠제이성형외과": { + "facebook": [ + "https://www.facebook.com/people/MJ%ED%94%BC%EB%B6%80%EA%B3%BC/100063928095304/" + ], + "blog": [ + "https://blog.naver.com/mjskinclinic" + ], + "youtube": [ + "https://www.youtube.com/channel/UCFjkFyYDu4HpLjc9axlh6YQ" + ], + "cafe": [ + "https://cafe.naver.com/beauteparadis" + ] + }, + "아우라성형외과": { + "blog": [ + "https://blog.naver.com/pristor" + ] + }, + "하이봄성형외과": { + "facebook": [ + "https://www.facebook.com/highvom1" + ], + "blog": [ + "https://blog.naver.com/hivom" + ], + "youtube": [ + "https://www.youtube.com/channel/UCuuH9oWh9h29hVRmH2sHAuA" + ], + "instagram": [ + "https://www.instagram.com/highvom", + "https://www.instagram.com/highvom/" + ] + }, + "유앤아이성형외과": { + "facebook": [ + "https://www.facebook.com/uni114" + ], + "youtube": [ + "https://www.youtube.com/channel/UCHgZnNk3JIDxnoYcWSWtAJw" + ], + "instagram": [ + "https://www.instagram.com/skinuni114/" + ] + }, + "리본성형외과": { + "facebook": [ + "https://www.facebook.com/reborn3355" + ], + "blog": [ + "https://blog.naver.com/reborn1999" + ], + "youtube": [ + "https://www.youtube.com/channel/UCyFdX9zxfu2e2JVTwPjki5A" + ], + "instagram": [ + "https://www.instagram.com/rebornps" + ] + }, + "라이크성형외과": { + "facebook": [ + "https://www.facebook.com/%EB%9D%BC%EC%9D%B4%ED%81%AC%EC%84%B1%ED%98%95%EC%99%B8%EA%B3%BC-101498222230862", + "https://www.facebook.com/likeps1" + ], + "blog": [ + "https://blog.naver.com/likeps0012" + ], + "youtube": [ + "https://www.youtube.com/channel/UCZ1kKCfEYUxCj78VPEC_wow" + ], + "instagram": [ + "https://www.instagram.com/likeps12", + "https://www.instagram.com/like_likeps", + "https://www.instagram.com/likeps_kr" + ], + "cafe": [ + "https://cafe.naver.com/plasticsurg", + "http://cafe.daum.net/plasticsurg" + ] + }, + "케이플러스성형외과": { + "facebook": [ + "https://www.facebook.com/kclinics" + ], + "blog": [ + "https://blog.naver.com/kclinics-osh" + ], + "youtube": [ + "https://www.youtube.com/@k-plasticsurgery" + ], + "instagram": [ + "https://www.instagram.com/k_plasticsurgery/" + ] + }, + "케이아트성형외과": { + "facebook": [ + "https://www.facebook.com/kartpsseoul" + ], + "blog": [ + "https://blog.naver.com/kartps" + ], + "youtube": [ + "https://www.youtube.com/channel/UCHs9LOYtauBIklXhPPZBAgA", + "https://www.youtube.com/channel/UCHs9LOYtauBIklXhPPZBAgA/videos?disable_polymer=1%3F%3F" + ], + "instagram": [ + "https://www.instagram.com/kartps/" + ] + }, + "이룸성형외과": { + "facebook": [ + "https://www.facebook.com/서울아이성형외과-105199207892670/" + ], + "blog": [ + "https://blog.naver.com/seoulips" + ], + "youtube": [ + "https://www.youtube.com/@seouliplasticsurgery", + "https://www.youtube.com/channel/UCYcssdFunRLmD8b376Y6ykg" + ], + "instagram": [ + "https://www.instagram.com/seoulips/?hl=ko", + "https://www.instagram.com/seouli_ps_th/", + "https://www.instagram.com/seouli_jp/" + ], + "cafe": [ + "https://cafe.naver.com/seoulips" + ] + }, + "서초서울성형외과": { + "facebook": [ + "https://www.facebook.com/user128" + ] + }, + "서초연세성형외과": { + "blog": [ + "https://blog.naver.com/chaminst" + ], + "youtube": [ + "https://www.youtube.com/channel/UCMxBfZivKz5jjhRiy0mzOXQ" + ], + "instagram": [ + "https://www.instagram.com/chamin_ps" + ] + }, + "리모성형외과": { + "blog": [ + "https://blog.naver.com/shpsclinic", + "https://blog.naver.com/rraa0406" + ], + "youtube": [ + "https://www.youtube.com/channel/UCCiH9OffJgZTMnkxatK8-5A" + ], + "instagram": [ + "https://www.instagram.com/sh___ps/" + ] + }, + "화이트성형외과": { + "youtube": [ + "https://youtu.be/xxFDhNk2Scc", + "https://www.youtube.com/channel/UCd2CJ_TVHTV5qE0TXcscF3g" + ] + }, + "미호성형외과": { + "facebook": [ + "https://www.facebook.com/mihops5503" + ], + "blog": [ + "https://blog.naver.com/mihops5955503", + "https://blog.naver.com/miho3925ps", + "https://blog.naver.com/qalfuqbjtmnf" + ], + "youtube": [ + "https://www.youtube.com/channel/UC8kyWAnMM7f_xguyPxR_gtA", + "https://www.youtube.com/channel/UC8kyWAnMM7f_xguyPxR_gtA/featured", + "https://www.youtube.com/channel/UCefpyLZgvqZ4bqDofEcE45A" + ], + "instagram": [ + "https://www.instagram.com/mihops5503/", + "https://www.instagram.com/mihops_kr/", + "https://www.instagram.com/mihops5503", + "https://www.instagram.com/miho_realpic", + "https://www.instagram.com/premier__official/" + ], + "cafe": [ + "https://cafe.naver.com/thefoxstyle", + "https://cafe.naver.com/mihopsps" + ] + }, + "에비뉴성형외과": {}, + "비온성형외과": { + "blog": [ + "https://blog.naver.com/miz2199" + ], + "youtube": [ + "https://www.youtube.com/channel/UClCjGIfEb3b1N-Q5tCNSU5w" + ], + "instagram": [ + "https://api.instagram.com/v1" + ] + }, + "아크성형외과": { + "facebook": [ + "https://www.facebook.com/arcplasticsurgery", + "https://www.facebook.com/arcps.thai/" + ], + "blog": [ + "https://blog.naver.com/arcps" + ], + "youtube": [ + "https://www.youtube.com/channel/UC4Zc0k59qdbrzmNG1aDNmHQ/featured" + ], + "instagram": [ + "https://instagram.com/p/", + "https://www.instagram.com/arc_plastic_surgery/" + ] + }, + "에버성형외과": { + "facebook": [ + "https://www.facebook.com/everplasticsurgery/?ref=bookmarks", + "https://www.facebook.com/everplasticsurgery/", + "https://www.facebook.com/everplasticsurgery" + ], + "blog": [ + "http://blog.naver.com/drblue007", + "http://blog.naver.com/everclinic", + "https://blog.naver.com/drblue007" + ], + "youtube": [ + "https://www.youtube.com/channel/UCwSusNRTM2B_mvdRd0yNVcw" + ], + "instagram": [ + "https://www.instagram.com/everclinic0088/" + ] + }, + "유앤미성형외과": { + "youtube": [ + "https://www.youtube.com/channel/UCAROD-B1ZBinpyzql7o_IlA" + ], + "instagram": [ + "https://www.instagram.com/knyounmeclinic", + "https://www.instagram.com/knyounmeclinic/" + ] + }, + "마노성형외과": { + "blog": [ + "https://blog.naver.com/tkhr4747" + ], + "youtube": [ + "https://www.youtube.com/@mano_ps" + ], + "instagram": [ + "https://www.instagram.com/manops_official" + ] + }, + "에스엠성형외과": { + "facebook": [ + "https://www.facebook.com/thammySM3707?mibextid=LQQJ4d" + ], + "blog": [ + "https://blog.naver.com/smps1004", + "https://blog.naver.com/smps1004/223225872355", + "https://blog.naver.com/smps1004/223222618079", + "https://blog.naver.com/smps1004/223220646900", + "https://blog.naver.com/smps1004/223219865154", + "https://blog.naver.com/smps1004/223219923186", + "https://blog.naver.com/smps1004/223242618343" + ], + "youtube": [ + "https://www.youtube.com/@smpsclinic" + ], + "instagram": [ + "https://www.instagram.com/smps_plastic_surgery/?igshid=MzRlODBiNWFlZA%3D%3D", + "https://www.instagram.com/smps_id/?igsh=MW5seTU0ZzVnZ3Fndg%3D%3D" + ] + }, + "서울미작성형외과": { + "facebook": [ + "https://www.facebook.com/mijaknose" + ], + "blog": [ + "https://blog.naver.com/mijak3444" + ], + "instagram": [ + "https://www.instagram.com/mijak2444/" + ] + }, + "더원성형외과": { + "blog": [ + "https://blog.naver.com/brs0714" + ], + "youtube": [ + "https://youtu.be/awcUKK31Kis", + "https://youtu.be/jKY3iF3pc3Q", + "https://youtu.be/WhUSe5fiUWA", + "https://youtu.be/4QK2W0Md_HM" + ], + "instagram": [ + "https://www.instagram.com/theone_plastic_surgery/" + ] + } +} \ No newline at end of file diff --git a/docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md b/docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md index 3152cc9..cffc23f 100644 --- a/docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md +++ b/docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md @@ -310,6 +310,15 @@ INFINITH 저장소(브랜치 feature/discovery-url-cta)에서 "AI Discovery 자 - 원진 결과: 후보 427 → 저장 66장(의료진 17, 시설 9, 시술 40). 6편 전부 히어로 있음, 눈성형·상담 질문 글 영상 3편, 원장 17명 사진. 눈으로 확인한 것: 상담 장면·수술실 모니터링·코 수술 도해는 적합, 흰 배경 마케팅 문구 이미지(힙업 카피)는 1차에서 통과했다가 평탄화 순서 수정 후 제외. https://supporters-wonjin.vercel.app 재배포. - 한계: 이미지 내용을 사람이 전수 확인하지는 않았다(파일명·alt·흰 배경 휴리스틱). 얼굴·신체 검출은 없으므로 병원 송부 전 사람이 갤러리를 한 번 훑는다. 뉴스룸·감사 재실행은 아직. +### 11-12. 채널 발견 + 엔티티 검증 (v1 §3 Phase 0 대체) · 2026-09-08 +- haewon 요청: 병원 이름이나 URL만 들어오면 웹 검색으로 유튜브·소셜을 찾고, 맞는지 엔티티를 확인하고, 내용을 끌어와 사이트에 반영. +- `supporters/scripts/discover_channels.mjs --clinic (--name | --url) [--evidence] [--out]`. datacrawling 스킬 §1~§2 를 코드로 옮겼다. 다섯 갈래: ① 사이트 전수(홈·법무·소개·연락처 HTML, JSON-LD sameAs, og/twitter 메타, 근거 수집 링크 전체) ② 웹검색(Firecrawl search 9쿼리 + 네이버 webkr·blog + YouTube API 채널 검색) ③ 핸들 변형(도메인 base × 구분자 × 접미사, 존재 확인만) ④ 레지스트리(`data/clinic_social_registry.json`, 73곳) ⑤ 플랫폼 검색(YouTube API). 이름만 주면 "공식 홈페이지" 검색으로 URL 을 먼저 찾는다. +- 판정(3-state 이상): 홈페이지 역링크(경로 경계까지 일치, +3) · 프로필 제목에 상호(+2, 강남언니·바비톡·플레이스는 +3) · 프로필 설명에 홈페이지 도메인/전화(+2) · 레지스트리(+2) · 검색 스니펫(+1). 3 이상 CONFIRMED, 1~2 CANDIDATE(사람 확인), 404 HANDLE_DEAD, 차단 BLOCKED, 후보 0 은 다섯 갈래 전부 돌았을 때만 NOT_FOUND 아니면 UNVERIFIED. 유튜브는 채널 id 로 병합. +- 함정: 역링크를 부분 문자열로 보면 `instagram.com/wonjin` 이 `wonjin_official` 안에서 잡혀 변형 핸들이 전부 CONFIRMED 된다 → 경로 경계 정규식. 전화는 하이픈 형식만(사업자번호 오탐). +- 워커 `discover` 단계(data 다음): channels.json → 사이트 데이터, CONFIRMED 1순위를 factSheet.surfaces(유튜브·인스타·페북·블로그·틱톡·카카오·강남언니·플레이스·바비톡)에 넣어 MedicalClinic sameAs·푸터·방문 페이지에 반영. youtube 단계는 CONFIRMED 유튜브 중 구독자 최대를 자동 선택(`--youtube` 우선). status.report.channels 에 인벤토리·확정·후보 저장 → `/discovery/:id`·`/supporters/:id` 패널 "발견한 채널"(확인됨/확인 필요 배지)과 리포트 표. +- 원진(이름만 입력) 결과: 홈페이지 k-wonjin.co.kr 추정 → 유튜브 2(WJ원진성형외과 28,300 · @wjwonjin 14,700), 인스타 3(wonjin_official·wonjin_ps·wj_cosmetic), 페북 KwonjinPS, 블로그 popokpop, 틱톡 wonjin_official, 카카오, 강남언니 2500, 바비톡 3143 CONFIRMED · 네이버 플레이스 NOT_FOUND(사이트·검색에 링크 없음, 플레이스 내부 검색은 미실행이라 실제로는 UNVERIFIED 에 가깝다) · 후보 12(영문 채널·유세윤 광고 채널·wj_plasticsurgery 등). 라이브 sameAs 4개 확인. 뷰성형외과 회귀: 유튜브 2·인스타 4·페북·블로그·카카오 CONFIRMED. +- 미결: 유튜브 공식 채널이 2개(구독 28,300 / 14,700). 어느 쪽을 사이트 기준으로 쓸지 haewon·병원 확인. 지금은 haewon 이 준 UC-EBM(14,700) 집계본을 쓰고 있고, 새로 돌리면 구독자 많은 쪽을 고른다. + --- ## 12. haewon 결정 반영과 제안 (2026-09-07 세션 2) diff --git a/docs/reports/wonjin/01_readiness_report/Wonjin_AI_Answer_Readiness_Report_v2.0_2026-09-08.html b/docs/reports/wonjin/01_readiness_report/Wonjin_AI_Answer_Readiness_Report_v2.0_2026-09-08.html index bcf0f87..8303493 100644 --- a/docs/reports/wonjin/01_readiness_report/Wonjin_AI_Answer_Readiness_Report_v2.0_2026-09-08.html +++ b/docs/reports/wonjin/01_readiness_report/Wonjin_AI_Answer_Readiness_Report_v2.0_2026-09-08.html @@ -120,6 +120,11 @@
분류제목근거 영상병원 확인 대기상태
A원진성형외과는 어떤 병원인가요? 진료 분야·의료진 구성·위치0편5건의학 검토 대기
B원진성형외과 방문 안내: 위치·진료시간·주차·예약은 어떻게 하나요?0편3건의학 검토 대기
D원진성형외과 윤곽은 어떤 방법이 있고 시술 시간·마취·회복은 어떻게 되나요?0편5건의학 검토 대기
D원진성형외과 눈성형은 어떤 방법이 있고 시술 시간·마취·회복은 어떻게 되나요?3편5건의학 검토 대기
D원진성형외과 레이저안티에이징은 어떤 방법이 있고 시술 시간·마취·회복은 어떻게 되나요?0편6건의학 검토 대기
C상담에서 꼭 물어봐야 할 질문은 무엇인가요?3편3건의학 검토 대기
+

발견한 채널과 확인 결과

+

홈페이지 역링크, 프로필 제목·설명, 등록부, 검색 결과를 근거로 병원 것인지 판정했습니다. "확인 필요"는 병원이 맞다고 알려 주시면 연결합니다.

+ + +
플랫폼확인됨확인 필요
유튜브WJ원진성형외과 (구독 28,300)
WJ원진성형외과 (구독 14,700)
Wonjin Plastic Surgery Korea
유세윤의 광고100 X 원진성형외과
WONJINPS
Wonjin Aesthetics Centre
WONJIN_OFFICIAL - KOREA No.1 PLASTIC SURGERY
원트성형외과
인스타그램Instagram
Instagram
Instagram
Instagram
Instagram
Instagram
Instagram
Instagram
Instagram
Instagram
Instagram
Instagram
페이스북KwonjinPS
네이버 블로그popokpopnaramalssami1
qeqe0420
vpepwxormtfo
틱톡wonjin_official
카카오톡 채널원진성형외과의원
강남언니원진성형외과의원 | 강남언니 - 내 선택에 확신을
네이버 플레이스찾지 못함
바비톡원진성형외과의원 | 후기∙가격∙이벤트 정보 - 바비톡

병원이 확인해 주실 것 9가지

홈페이지 공개 자료에 없어 병원만 답할 수 있는 값입니다. 입력 전까지 사이트에는 "확인 대기"로 표시됩니다.

1 작성자 실명·직함·공개 프로필
2 글별 의학 검토 원장과 검토일
3 지원 관계 문구
4 견적 기본 포함 항목·결제 방식
5 회복 일정표의 병원 확정값
6 재수술 정책
7 페이지 간 표기 불일치의 정답
8 진료 분야별 담당 원장
9 정식 도메인·색인 허용 시점·홈페이지 링크 위치
diff --git a/docs/reports/wonjin/01_readiness_report/Wonjin_AI_Answer_Readiness_Report_v2.0_2026-09-08.pdf b/docs/reports/wonjin/01_readiness_report/Wonjin_AI_Answer_Readiness_Report_v2.0_2026-09-08.pdf index dbebd5e..40f73f4 100644 Binary files a/docs/reports/wonjin/01_readiness_report/Wonjin_AI_Answer_Readiness_Report_v2.0_2026-09-08.pdf and b/docs/reports/wonjin/01_readiness_report/Wonjin_AI_Answer_Readiness_Report_v2.0_2026-09-08.pdf differ diff --git a/docs/reports/wonjin/01_readiness_report/Wonjin_channels_2026-09-08.json b/docs/reports/wonjin/01_readiness_report/Wonjin_channels_2026-09-08.json new file mode 100644 index 0000000..fa950c8 --- /dev/null +++ b/docs/reports/wonjin/01_readiness_report/Wonjin_channels_2026-09-08.json @@ -0,0 +1,877 @@ +{ + "clinic": "wonjin", + "name": "원진성형외과", + "url": "https://www.k-wonjin.co.kr", + "checkedAt": "2026-09-08", + "phone": "02-3477-3300", + "branches": { + "sitePages": [ + "/", + "/service/private", + "/service/terms", + "/hospitalinfo/about", + "/sitemap.xml" + ], + "searchQueries": [ + "\"원진성형외과\" 인스타그램", + "\"원진성형외과\" 유튜브", + "\"원진성형외과\" 페이스북", + "\"원진성형외과\" 블로그", + "원진성형외과 강남언니", + "원진성형외과 네이버 플레이스", + "site:instagram.com 원진", + "site:youtube.com 원진성형외과", + "site:facebook.com 원진성형외과", + "naver:원진성형외과 인스타그램", + "naver:원진성형외과 유튜브", + "naver:원진성형외과 페이스북", + "naver:원진성형외과 블로그" + ], + "variantsTried": [ + "instagram:wonjin", + "instagram:wonjinofficial", + "instagram:wonjin.official", + "instagram:wonjin-official", + "instagram:wonjinps", + "instagram:wonjin.ps", + "instagram:wonjin-ps", + "instagram:wonjinclinic", + "instagram:wonjin.clinic", + "instagram:wonjin_clinic", + "instagram:wonjin-clinic", + "instagram:wonjinplasticsurgery", + "instagram:wonjin.plasticsurgery", + "instagram:wonjin_plasticsurgery", + "instagram:wonjin-plasticsurgery", + "instagram:wonjinkr", + "instagram:wonjin.kr", + "instagram:wonjin_kr", + "instagram:wonjin-kr", + "instagram:wonjinkorea", + "instagram:wonjin.korea", + "instagram:wonjin_korea", + "instagram:wonjin-korea", + "instagram:wonjinseoul", + "instagram:wonjin.seoul", + "instagram:wonjin_seoul", + "instagram:wonjin-seoul", + "youtube:wonjin", + "youtube:wonjinofficial", + "youtube:wonjin.official", + "youtube:wonjin_official", + "youtube:wonjin-official", + "youtube:wonjinps", + "youtube:wonjin.ps", + "youtube:wonjin_ps", + "youtube:wonjin-ps", + "youtube:wonjinclinic", + "youtube:wonjin.clinic", + "youtube:wonjin_clinic", + "youtube:wonjin-clinic", + "youtube:wonjinplasticsurgery" + ], + "registry": true, + "allBranches": true + }, + "inventory": [ + { + "platform": "youtube", + "state": "CONFIRMED", + "confirmed": 2, + "candidates": 6, + "tried": "사이트 5면 · 검색 13건 · 변형 14" + }, + { + "platform": "instagram", + "state": "CONFIRMED", + "confirmed": 3, + "candidates": 9, + "tried": "사이트 5면 · 검색 13건 · 변형 27" + }, + { + "platform": "facebook", + "state": "CONFIRMED", + "confirmed": 1, + "candidates": 0, + "tried": "사이트 5면 · 검색 13건 · 변형 0" + }, + { + "platform": "naverBlog", + "state": "CONFIRMED", + "confirmed": 1, + "candidates": 3, + "tried": "사이트 5면 · 검색 13건 · 변형 0" + }, + { + "platform": "tiktok", + "state": "CONFIRMED", + "confirmed": 1, + "candidates": 0, + "tried": "사이트 5면 · 검색 13건 · 변형 0" + }, + { + "platform": "kakao", + "state": "CONFIRMED", + "confirmed": 1, + "candidates": 0, + "tried": "사이트 5면 · 검색 13건 · 변형 0" + }, + { + "platform": "gangnamunni", + "state": "CONFIRMED", + "confirmed": 1, + "candidates": 0, + "tried": "사이트 5면 · 검색 13건 · 변형 0" + }, + { + "platform": "naverPlace", + "state": "NOT_FOUND", + "confirmed": 0, + "candidates": 0, + "tried": "사이트 5면 · 검색 13건 · 변형 0" + }, + { + "platform": "babitalk", + "state": "CONFIRMED", + "confirmed": 1, + "candidates": 0, + "tried": "사이트 5면 · 검색 13건 · 변형 0" + } + ], + "platforms": { + "youtube": [ + { + "platform": "youtube", + "key": "UCNBaQBCok5xIViCnrGdisoQ", + "url": "https://www.youtube.com/channel/UCNBaQBCok5xIViCnrGdisoQ", + "status": "CONFIRMED", + "score": 9, + "evidence": [ + "홈페이지에서 역링크", + "레지스트리 등재", + "프로필 제목에 상호: \"WJ원진성형외과\"", + "프로필 설명에 홈페이지 도메인" + ], + "sources": [ + "site", + "site:sameAs", + "registry", + "youtube:search", + "search" + ], + "title": "WJ원진성형외과", + "handle": "@wj2541", + "channelId": "UCNBaQBCok5xIViCnrGdisoQ", + "subscribers": 28300, + "videos": 62, + "views": 33899424 + }, + { + "platform": "youtube", + "key": "@wjwonjin", + "url": "https://www.youtube.com/@wjwonjin", + "status": "CONFIRMED", + "score": 9, + "evidence": [ + "홈페이지에서 역링크", + "레지스트리 등재", + "프로필 제목에 상호: \"WJ원진성형외과\"", + "프로필 설명에 홈페이지 도메인" + ], + "sources": [ + "site", + "site:sameAs", + "registry", + "search", + "youtube:search" + ], + "title": "WJ원진성형외과", + "handle": "@wjwonjin", + "channelId": "UC-EBM4e0VST1W_PQ0NzFMQg", + "subscribers": 14700, + "videos": 736, + "views": 31128302 + }, + { + "platform": "youtube", + "key": "UCNmTm4UubrscPczrAfGhUvQ", + "url": "https://www.youtube.com/channel/UCNmTm4UubrscPczrAfGhUvQ", + "status": "CANDIDATE", + "score": 2, + "evidence": [ + "프로필 제목에 상호: \"Wonjin Plastic Surgery Korea\"" + ], + "sources": [ + "youtube:search" + ], + "title": "Wonjin Plastic Surgery Korea", + "handle": "@wonjinplasticsurgerykorea9746", + "channelId": "UCNmTm4UubrscPczrAfGhUvQ", + "subscribers": 146000, + "videos": 50, + "views": 8590 + }, + { + "platform": "youtube", + "key": "UCk2UbMID_NjYZe7-WBLQXRw", + "url": "https://www.youtube.com/channel/UCk2UbMID_NjYZe7-WBLQXRw", + "status": "CANDIDATE", + "score": 2, + "evidence": [ + "프로필 제목에 상호: \"유세윤의 광고100 X 원진성형외과\"" + ], + "sources": [ + "youtube:search" + ], + "title": "유세윤의 광고100 X 원진성형외과", + "handle": "@유세윤의광고100x원진", + "channelId": "UCk2UbMID_NjYZe7-WBLQXRw", + "subscribers": 736, + "videos": 4, + "views": 220740 + }, + { + "platform": "youtube", + "key": "UCEC21_7WvRx8cWlje4sLVlw", + "url": "https://www.youtube.com/channel/UCEC21_7WvRx8cWlje4sLVlw", + "status": "CANDIDATE", + "score": 2, + "evidence": [ + "프로필 제목에 상호: \"WONJINPS\"" + ], + "sources": [ + "youtube:search", + "variant" + ], + "title": "WONJINPS", + "handle": "@wonjinps", + "channelId": "UCEC21_7WvRx8cWlje4sLVlw", + "subscribers": 40, + "videos": 14, + "views": 28223 + }, + { + "platform": "youtube", + "key": "UC8PY7tOsvoaV_z8Rlp_it0Q", + "url": "https://www.youtube.com/channel/UC8PY7tOsvoaV_z8Rlp_it0Q", + "status": "CANDIDATE", + "score": 2, + "evidence": [ + "프로필 제목에 상호: \"Wonjin Aesthetics Centre\"" + ], + "sources": [ + "youtube:search" + ], + "title": "Wonjin Aesthetics Centre", + "handle": "@wonjinaestheticscentre3458", + "channelId": "UC8PY7tOsvoaV_z8Rlp_it0Q", + "subscribers": 5, + "videos": 11, + "views": 2831 + }, + { + "platform": "youtube", + "key": "UCJuM0rYynWd9xAXmOf-dl9A", + "url": "https://www.youtube.com/channel/UCJuM0rYynWd9xAXmOf-dl9A", + "status": "CANDIDATE", + "score": 2, + "evidence": [ + "프로필 제목에 상호: \"WONJIN_OFFICIAL - KOREA No.1 PLASTIC SUR\"" + ], + "sources": [ + "variant" + ], + "title": "WONJIN_OFFICIAL - KOREA No.1 PLASTIC SURGERY", + "handle": "@wonjin_official", + "channelId": "UCJuM0rYynWd9xAXmOf-dl9A", + "subscribers": 3, + "videos": 1, + "views": 28 + }, + { + "platform": "youtube", + "key": "UChnJVIjdmUeZkA40cRFMzFA", + "url": "https://www.youtube.com/channel/UChnJVIjdmUeZkA40cRFMzFA", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (naver:원진성형외과 유튜브)" + ], + "sources": [ + "search" + ], + "title": "원트성형외과", + "handle": "@want_2025", + "channelId": "UChnJVIjdmUeZkA40cRFMzFA", + "subscribers": 79, + "videos": 58, + "views": 98233 + } + ], + "instagram": [ + { + "platform": "instagram", + "key": "wonjin_official", + "url": "https://www.instagram.com/wonjin_official/", + "status": "CONFIRMED", + "score": 6, + "evidence": [ + "홈페이지에서 역링크", + "레지스트리 등재", + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 인스타그램)" + ], + "sources": [ + "site", + "site:sameAs", + "registry", + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin_ps", + "url": "https://www.instagram.com/wonjin_ps/", + "status": "CONFIRMED", + "score": 6, + "evidence": [ + "홈페이지에서 역링크", + "레지스트리 등재", + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 인스타그램)" + ], + "sources": [ + "site", + "site:sameAs", + "registry", + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wj_cosmetic", + "url": "https://www.instagram.com/wj_cosmetic/", + "status": "CONFIRMED", + "score": 5, + "evidence": [ + "홈페이지에서 역링크", + "레지스트리 등재" + ], + "sources": [ + "site", + "site:sameAs", + "registry" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wj_plasticsurgery", + "url": "https://www.instagram.com/wj_plasticsurgery/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 인스타그램)" + ], + "sources": [ + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "hamdust_", + "url": "https://www.instagram.com/hamdust_/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (site:instagram.com 원진)" + ], + "sources": [ + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "bjwnjn", + "url": "https://www.instagram.com/bjwnjn/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (site:instagram.com 원진)" + ], + "sources": [ + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin_sunnywon", + "url": "https://www.instagram.com/wonjin_sunnywon/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (site:instagram.com 원진)" + ], + "sources": [ + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjinire", + "url": "https://www.instagram.com/wonjinire/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (site:instagram.com 원진)" + ], + "sources": [ + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wxxnzin", + "url": "https://www.instagram.com/wxxnzin/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (site:instagram.com 원진)" + ], + "sources": [ + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wj_1j_", + "url": "https://www.instagram.com/wj_1j_/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (site:instagram.com 원진)" + ], + "sources": [ + "search" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin", + "url": "https://www.instagram.com/wonjin/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 인스타그램)" + ], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjinps", + "url": "https://www.instagram.com/wonjinps/", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 페이스북)" + ], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjinofficial", + "url": "https://www.instagram.com/wonjinofficial/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin.official", + "url": "https://www.instagram.com/wonjin.official/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin.ps", + "url": "https://www.instagram.com/wonjin.ps/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjinclinic", + "url": "https://www.instagram.com/wonjinclinic/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin.clinic", + "url": "https://www.instagram.com/wonjin.clinic/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin_clinic", + "url": "https://www.instagram.com/wonjin_clinic/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjinplasticsurgery", + "url": "https://www.instagram.com/wonjinplasticsurgery/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin.plasticsurgery", + "url": "https://www.instagram.com/wonjin.plasticsurgery/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin_plasticsurgery", + "url": "https://www.instagram.com/wonjin_plasticsurgery/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjinkr", + "url": "https://www.instagram.com/wonjinkr/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin.kr", + "url": "https://www.instagram.com/wonjin.kr/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin_kr", + "url": "https://www.instagram.com/wonjin_kr/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjinkorea", + "url": "https://www.instagram.com/wonjinkorea/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin.korea", + "url": "https://www.instagram.com/wonjin.korea/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin_korea", + "url": "https://www.instagram.com/wonjin_korea/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjinseoul", + "url": "https://www.instagram.com/wonjinseoul/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin.seoul", + "url": "https://www.instagram.com/wonjin.seoul/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + }, + { + "platform": "instagram", + "key": "wonjin_seoul", + "url": "https://www.instagram.com/wonjin_seoul/", + "status": "UNRELATED", + "score": 0, + "evidence": [], + "sources": [ + "variant" + ], + "title": "Instagram" + } + ], + "facebook": [ + { + "platform": "facebook", + "key": "KwonjinPS", + "url": "https://www.facebook.com/KwonjinPS", + "status": "CONFIRMED", + "score": 6, + "evidence": [ + "홈페이지에서 역링크", + "레지스트리 등재", + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 페이스북)" + ], + "sources": [ + "site", + "site:sameAs", + "registry", + "search" + ] + }, + { + "platform": "facebook", + "key": "photo.php", + "url": "https://www.facebook.com/photo.php", + "status": "HANDLE_DEAD", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 페이스북)" + ], + "sources": [ + "search" + ] + } + ], + "naverBlog": [ + { + "platform": "naverBlog", + "key": "popokpop", + "url": "https://blog.naver.com/popokpop", + "status": "CONFIRMED", + "score": 6, + "evidence": [ + "홈페이지에서 역링크", + "레지스트리 등재", + "검색 결과 제목/요약에 상호 (원진성형외과 네이버 플레이스)" + ], + "sources": [ + "site", + "site:sameAs", + "registry", + "search" + ], + "title": "" + }, + { + "platform": "naverBlog", + "key": "naramalssami1", + "url": "https://blog.naver.com/naramalssami1", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 블로그)" + ], + "sources": [ + "search" + ], + "title": "" + }, + { + "platform": "naverBlog", + "key": "qeqe0420", + "url": "https://blog.naver.com/qeqe0420", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 블로그)" + ], + "sources": [ + "search" + ], + "title": "" + }, + { + "platform": "naverBlog", + "key": "vpepwxormtfo", + "url": "https://blog.naver.com/vpepwxormtfo", + "status": "CANDIDATE", + "score": 1, + "evidence": [ + "검색 결과 제목/요약에 상호 (원진성형외과 네이버 플레이스)" + ], + "sources": [ + "search" + ], + "title": "" + } + ], + "tiktok": [ + { + "platform": "tiktok", + "key": "wonjin_official", + "url": "https://www.tiktok.com/@wonjin_official", + "status": "CONFIRMED", + "score": 4, + "evidence": [ + "홈페이지에서 역링크", + "검색 결과 제목/요약에 상호 (\"원진성형외과\" 인스타그램)" + ], + "sources": [ + "site", + "site:sameAs" + ], + "title": "" + } + ], + "kakao": [ + { + "platform": "kakao", + "key": "_bxflzE", + "url": "https://pf.kakao.com/_bxflzE", + "status": "CONFIRMED", + "score": 5, + "evidence": [ + "홈페이지에서 역링크", + "프로필 제목에 상호: \"원진성형외과의원\"" + ], + "sources": [ + "site", + "search" + ], + "title": "원진성형외과의원" + } + ], + "gangnamunni": [ + { + "platform": "gangnamunni", + "key": "2500", + "url": "https://www.gangnamunni.com/hospitals/2500", + "status": "CONFIRMED", + "score": 3, + "evidence": [ + "프로필 제목에 상호: \"원진성형외과의원 | 강남언니 - 내 선택에 확신을\"" + ], + "sources": [ + "search" + ], + "title": "원진성형외과의원 | 강남언니 - 내 선택에 확신을" + } + ], + "naverPlace": [], + "babitalk": [ + { + "platform": "babitalk", + "key": "3143", + "url": "https://babitalk.com/hospitals/3143", + "status": "CONFIRMED", + "score": 3, + "evidence": [ + "프로필 제목에 상호: \"원진성형외과의원 | 후기∙가격∙이벤트 정보 - 바비톡\"" + ], + "sources": [ + "search" + ], + "title": "원진성형외과의원 | 후기∙가격∙이벤트 정보 - 바비톡" + } + ] + }, + "rules": "datacrawling 스킬 §1~§2. CONFIRMED=근거 점수 3 이상, CANDIDATE=사람 확인, NOT_FOUND 는 다섯 갈래 전부 실행 시에만" +} \ No newline at end of file diff --git a/scripts/build_discovery_report.ts b/scripts/build_discovery_report.ts index c43de33..58553d0 100644 --- a/scripts/build_discovery_report.ts +++ b/scripts/build_discovery_report.ts @@ -166,6 +166,11 @@ ${status ? `
${posts.map((p: any) => { const b = briefById.get(p.id); return ``; }).join('')}
분류제목근거 영상병원 확인 대기상태
${esc(p.category)}${esc(p.title)}${status.previewUrl ? `` : ''}${(b?.videos ?? []).length}편${(p.pending ?? []).length}건의학 검토 대기
+ ${status.report?.channels ? `

발견한 채널과 확인 결과

+

홈페이지 역링크, 프로필 제목·설명, 등록부, 검색 결과를 근거로 병원 것인지 판정했습니다. "확인 필요"는 병원이 맞다고 알려 주시면 연결합니다.

+ + ${(['youtube', 'instagram', 'facebook', 'naverBlog', 'tiktok', 'kakao', 'gangnamunni', 'naverPlace', 'babitalk'] as const).map((p) => { const L: Record = { youtube: '유튜브', instagram: '인스타그램', facebook: '페이스북', naverBlog: '네이버 블로그', tiktok: '틱톡', kakao: '카카오톡 채널', gangnamunni: '강남언니', naverPlace: '네이버 플레이스', babitalk: '바비톡' }; const ok = status.report.channels.confirmed[p] ?? []; const cd = status.report.channels.candidates[p] ?? []; return ``; }).join('')} +
플랫폼확인됨확인 필요
${L[p]}${ok.length ? ok.map((c: any) => `${esc(c.title || c.key)}${c.subscribers ? ` (구독 ${Number(c.subscribers).toLocaleString('ko-KR')})` : ''}`).join('
') : '찾지 못함'}
${cd.map((c: any) => `${esc(c.title || c.key)}`).join('
') || ''}
` : ''}

병원이 확인해 주실 것 ${INPUTS.length}가지

홈페이지 공개 자료에 없어 병원만 답할 수 있는 값입니다. 입력 전까지 사이트에는 "확인 대기"로 표시됩니다.

${INPUTS.map((t, i) => `
${i + 1} ${esc(t)}
`).join('')}
diff --git a/src/components/discovery/ClinicInputsPanel.tsx b/src/components/discovery/ClinicInputsPanel.tsx index 251ed99..6d2b2b3 100644 --- a/src/components/discovery/ClinicInputsPanel.tsx +++ b/src/components/discovery/ClinicInputsPanel.tsx @@ -15,7 +15,16 @@ type Build = { phase: string | null; preview_url: string | null; posts: Array<{ id: string; title: string; category: string; status?: string; reviewStatus?: string; pending?: string[] }>; - report: { physicians?: Array<{ id: string; name: string; title: string }>; generator?: Record; totalCostUsd?: number } | null; + report: { + physicians?: Array<{ id: string; name: string; title: string }>; + generator?: Record; + totalCostUsd?: number; + channels?: { + inventory: Array<{ platform: string; state: string; confirmed: number; candidates: number; tried: string }>; + confirmed: Record>; + candidates: Record>; + }; + } | null; updated_at: string; }; type InputRow = { key: string; post_id: string | null; value: Record; input_by: string | null; created_at: string }; @@ -162,6 +171,27 @@ export function ClinicInputsPanel({ clinicId }: { clinicId: string }) { {/* 글별 검토·승인 */}
+ {build?.report?.channels && ( +
+

발견한 채널

+

홈페이지·웹검색·레지스트리에서 찾아 병원 것인지 확인한 채널입니다. "확인 필요"는 병원이 맞는지 알려 주셔야 사이트에 연결합니다.

+
    + {(['youtube', 'instagram', 'facebook', 'naverBlog', 'tiktok', 'kakao', 'gangnamunni', 'naverPlace', 'babitalk'] as const).map((p) => { + const ok = build.report?.channels?.confirmed[p] ?? []; + const cand = build.report?.channels?.candidates[p] ?? []; + const label: Record = { youtube: '유튜브', instagram: '인스타그램', facebook: '페이스북', naverBlog: '네이버 블로그', tiktok: '틱톡', kakao: '카카오톡 채널', gangnamunni: '강남언니', naverPlace: '네이버 플레이스', babitalk: '바비톡' }; + if (!ok.length && !cand.length) return
  • {label[p]} · 찾지 못함
  • ; + return ( +
  • + {label[p]} + {ok.map((c) => {c.title || c.key}확인됨)} + {cand.map((c) => {c.title || c.key}확인 필요)} +
  • + ); + })} +
+
+ )}

의료진 ID

담당 원장·검토자 입력에 쓰는 ID 입니다. 홈페이지 의료진 페이지에서 읽었습니다.

diff --git a/supporters/scripts/discover_channels.mjs b/supporters/scripts/discover_channels.mjs new file mode 100644 index 0000000..d858612 --- /dev/null +++ b/supporters/scripts/discover_channels.mjs @@ -0,0 +1,210 @@ +// 채널 발견 + 엔티티 검증 (datacrawling 스킬 §1~§2 를 코드로). 병원 이름 또는 URL 하나로 유튜브·인스타·페이스북·네이버 블로그·틱톡·카카오·강남언니·네이버 플레이스를 찾고, +// 후보마다 "이 병원의 것인가"를 근거 점수로 판정한다. 부재는 다섯 갈래를 전부 돈 뒤에만 NOT_FOUND, 아니면 UNVERIFIED. +// +// node scripts/discover_channels.mjs --clinic (--name <상호> | --url <홈>) [--evidence ] [--out channels.json] [--registry ../data/clinic_social_registry.json] +// +// 갈래: ① 사이트 전수(홈·법무·소개·연락처 HTML + JSON-LD sameAs + 근거 수집 링크) ② 웹검색(Firecrawl search·네이버 webkr/blog·YouTube API 채널 검색) +// ③ 핸들 변형(도메인 기반 base × 구분자 × 접미사, 존재 확인만) ④ 레지스트리(data/clinic_social_registry.json) ⑤ 플랫폼 내부 검색(YouTube API) +// 판정: 상호 역링크(+3) · 프로필 제목에 상호(+2) · 프로필 설명에 홈페이지 도메인/전화(+2) · 레지스트리(+2) · 검색 스니펫에 상호+플랫폼(+1) +// ≥3 CONFIRMED · 1~2 CANDIDATE(사람 확인) · 존재 404 HANDLE_DEAD · 접근 차단 BLOCKED · 후보 0 NOT_FOUND(전 갈래 실행 시)/UNVERIFIED +import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs'; +import { join, resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = fileURLToPath(new URL('.', import.meta.url)); +const args = process.argv.slice(2); +const opt = (k, d) => { const i = args.indexOf(`--${k}`); return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : d; }; +(function loadEnv() { let dir = here; for (let i = 0; i < 4; i++) { const p = join(dir, '.env'); if (existsSync(p)) for (const line of readFileSync(p, 'utf8').split('\n')) { const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, ''); } dir = dirname(dir); } })(); +const KEYS = { yt: process.env.YOUTUBE_API_KEY, fc: process.env.FIRECRAWL_API_KEY, nid: process.env.NAVER_CLIENT_ID, nsec: process.env.NAVER_CLIENT_SECRET }; +const clinic = opt('clinic'); let name = opt('name'); let home = opt('url'); +if (!clinic || (!name && !home)) { console.error('--clinic 과 --name 또는 --url 필요'); process.exit(2); } +const OUT = resolve(opt('out', `channels.${clinic}.json`)); +const EV = opt('evidence') ? resolve(opt('evidence')) : null; +const REG = resolve(opt('registry', join(here, '../../data/clinic_social_registry.json'))); +const today = new Date().toISOString().slice(0, 10); +const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36 INFINITH-supporters-discover/1.0 (+mailto:o2oteam@o2o.kr)'; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const log = (s) => console.log(s); + +// ---------- 플랫폼 패턴 (supabase/functions/_shared/extractSocialLinks.ts 이식 + 병의원 표면) ---------- +const PLATFORMS = { + youtube: { re: /(?:www\.|m\.)?youtube\.com\/(?:@([a-zA-Z0-9._-]+)|channel\/(UC[a-zA-Z0-9_-]{20,})|c\/([a-zA-Z0-9._-]+)|user\/([a-zA-Z0-9._-]+))/i, key: (m) => (m[2] ? m[2] : '@' + (m[1] || m[3] || m[4])), url: (k) => (k.startsWith('UC') ? `https://www.youtube.com/channel/${k}` : `https://www.youtube.com/${k}`) }, + instagram: { re: /(?:www\.)?instagram\.com\/([a-zA-Z0-9._]{2,30})\/?(?:[?#]|$)/i, key: (m) => m[1].toLowerCase(), url: (k) => `https://www.instagram.com/${k}/`, skip: /^(p|reel|reels|explore|accounts|stories|share|about|legal|directory|tv)$/i }, + facebook: { re: /(?:www\.|m\.)?facebook\.com\/([a-zA-Z0-9._-]{2,60})\/?(?:[?#]|$)/i, key: (m) => m[1], url: (k) => `https://www.facebook.com/${k}`, skip: /^(sharer|share|login|help|pages|events|groups|marketplace|watch|gaming|privacy|policies|tr|dialog|plugins|photo|video|reel|profile\.php|people|hashtag|search)$/i }, + naverBlog: { re: /blog\.naver\.com\/([a-zA-Z0-9_-]{2,40})/i, key: (m) => m[1].toLowerCase(), url: (k) => `https://blog.naver.com/${k}`, skip: /^(PostView|BlogHome|postview)$/i }, + tiktok: { re: /(?:www\.)?tiktok\.com\/@([a-zA-Z0-9._-]+)/i, key: (m) => m[1].toLowerCase(), url: (k) => `https://www.tiktok.com/@${k}` }, + kakao: { re: /pf\.kakao\.com\/([a-zA-Z0-9_-]+)/i, key: (m) => m[1], url: (k) => `https://pf.kakao.com/${k}` }, + gangnamunni: { re: /gangnamunni\.com\/hospitals\/(\d+)/i, key: (m) => m[1], url: (k) => `https://www.gangnamunni.com/hospitals/${k}` }, + naverPlace: { re: /(?:m\.)?place\.naver\.com\/(?:hospital|place)\/(\d+)|map\.naver\.com\/(?:p\/entry\/place|v5\/entry\/place)\/(\d+)/i, key: (m) => m[1] || m[2], url: (k) => `https://m.place.naver.com/hospital/${k}` }, + babitalk: { re: /babitalk\.com\/hospitals\/(\d+)/i, key: (m) => m[1], url: (k) => `https://babitalk.com/hospitals/${k}` }, +}; +const cands = Object.fromEntries(Object.keys(PLATFORMS).map((p) => [p, new Map()])); // platform → key → { key, url, sources:Set, hits:[] } +function addCandidate(rawUrl, source, note = '') { + for (const [p, def] of Object.entries(PLATFORMS)) { + const m = String(rawUrl).match(def.re); if (!m) continue; + const k = def.key(m); if (!k || (def.skip && def.skip.test(k))) return; + const c = cands[p].get(k) ?? { platform: p, key: k, url: def.url(k), sources: new Set(), notes: [] }; + c.sources.add(source); if (note) c.notes.push(note); cands[p].set(k, c); return; + } +} +const extractAll = (text, source, note) => { for (const m of String(text).matchAll(/https?:\/\/[^\s"'<>)\]]+/g)) addCandidate(m[0], source, note); }; + +// ---------- 0. 이름 ↔ URL 보완 ---------- +async function fetchText(url, tries = 1) { + for (let i = 0; i <= tries; i++) { + try { const r = await fetch(url, { headers: { 'user-agent': UA, accept: 'text/html,*/*' }, redirect: 'follow', signal: AbortSignal.timeout(20000) }); return { status: r.status, url: r.url, body: r.ok ? await r.text() : '' }; } + catch (e) { if (i === tries) return { status: 0, url, body: '', error: e.message }; await sleep(800); } + } +} +async function fcSearch(query, limit = 8) { + if (!KEYS.fc) return []; + try { const r = await fetch('https://api.firecrawl.dev/v1/search', { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${KEYS.fc}` }, body: JSON.stringify({ query, limit }), signal: AbortSignal.timeout(30000) }); const j = await r.json(); return (j.data ?? []).map((x) => ({ url: x.url, title: x.title ?? '', snippet: x.description ?? '' })); } catch { return []; } +} +async function naverSearch(kind, query, display = 10) { + if (!KEYS.nid) return []; + try { const r = await fetch(`https://openapi.naver.com/v1/search/${kind}.json?query=${encodeURIComponent(query)}&display=${display}`, { headers: { 'X-Naver-Client-Id': KEYS.nid, 'X-Naver-Client-Secret': KEYS.nsec } }); const j = await r.json(); return (j.items ?? []).map((x) => ({ url: x.link, title: (x.title ?? '').replace(/<[^>]+>/g, ''), snippet: (x.description ?? '').replace(/<[^>]+>/g, '') })); } catch { return []; } +} +const PLATFORM_HOST = /youtube\.com|youtu\.be|instagram\.com|facebook\.com|blog\.naver\.com|tiktok\.com|kakao\.com|gangnamunni\.com|place\.naver\.com|map\.naver\.com|babitalk\.com|naver\.com|google\.|daum\.net|tistory\.com|namu\.wiki|wikipedia/i; + +const searchLog = []; +if (!home) { + // 상호로 공식 홈페이지 찾기: 플랫폼이 아닌 도메인 중 제목에 상호가 들어간 첫 결과 + const q = `${name} 공식 홈페이지`; searchLog.push(q); + const res = [...(await fcSearch(q, 8)), ...(await naverSearch('webkr', name, 10))]; + const core = coreName(name); + const hit = res.find((r) => !PLATFORM_HOST.test(r.url) && (r.title.replace(/\s/g, '').includes(core) || r.snippet.replace(/\s/g, '').includes(core))); + if (!hit) { console.error(`홈페이지를 찾지 못함: ${name}`); } + else { const u = new URL(hit.url); home = `${u.protocol}//${u.host}`; log(`홈페이지 추정: ${home} (${hit.title.slice(0, 40)})`); } +} +const homeHtml = home ? await fetchText(home) : { body: '' }; +if (!name) { + const t = homeHtml.body.match(/]*>([^<]*)<\/title>/i)?.[1] ?? ''; + const og = homeHtml.body.match(/property="og:site_name"\s+content="([^"]+)"/i)?.[1] ?? ''; + name = (og || t).replace(/[|–-].*$/, '').replace(/당신만 봅니다/, '').trim() || clinic; + log(`상호 추정: ${name}`); +} +function coreName(n) { return String(n).replace(/\s/g, '').replace(/(성형외과|의원|피부과|클리닉|병원|치과|한의원|WJ|wj)/gi, '') || String(n).replace(/\s/g, ''); } +const core = coreName(name); +const domainBase = home ? new URL(home).host.replace(/^www\./, '').split('.')[0].replace(/^k-|^the-?/, '') : ''; +const phone = (homeHtml.body.match(/0\d{1,2}-\d{3,4}-\d{4}/g) ?? [])[0] ?? ''; +log(`상호 ${name} · 핵심어 ${core} · 도메인 base ${domainBase} · 전화 ${phone || '?'}`); + +// ---------- ① 사이트 전수 ---------- +const sitePages = []; +if (home) { + const paths = ['/', '/privacy', '/privacy-policy', '/service/private', '/terms', '/service/terms', '/about', '/hospitalinfo/about', '/contact', '/counsel', '/sitemap.xml']; + for (const p of paths) { + const r = p === '/' ? { ...homeHtml, url: home } : await fetchText(home + p); + if (r.status && r.status < 400 && r.body) { + sitePages.push(p); + extractAll(r.body, 'site', p); + for (const m of r.body.matchAll(/"sameAs"\s*:\s*(\[[^\]]*\]|"[^"]+")/g)) extractAll(m[1], 'site:sameAs', p); + for (const m of r.body.matchAll(/]+(?:property|name)="(?:og:see_also|twitter:site)"[^>]+content="([^"]+)"/g)) extractAll(m[1], 'site:meta', p); + } + await sleep(200); + } +} +if (EV && existsSync(join(EV, 'index.json'))) { + const idx = JSON.parse(readFileSync(join(EV, 'index.json'), 'utf8')); + for (const p of idx.pages) { try { const pg = JSON.parse(readFileSync(join(EV, p.file), 'utf8')); for (const l of pg.links ?? []) addCandidate(String(l.href ?? l.url ?? l ?? ''), 'evidence', p.url); } catch { /* */ } } +} +const homeReciprocal = (homeHtml.body || '').toLowerCase(); + +// ---------- ④ 레지스트리 ---------- +let registry = null; +if (existsSync(REG)) { + const reg = JSON.parse(readFileSync(REG, 'utf8')); + const key = Object.keys(reg).find((k) => k.replace(/\s/g, '') === name.replace(/\s/g, '') || coreName(k) === core); + if (key) { registry = reg[key]; for (const list of Object.values(registry)) for (const u of list ?? []) addCandidate(u, 'registry'); log(`레지스트리: ${key} (${Object.values(registry).flat().length}개 URL)`); } +} + +// ---------- ② 웹검색 + ⑤ 플랫폼 검색 ---------- +const searchHits = []; // { url, title, snippet, query } +const queries = [ + `"${name}" 인스타그램`, `"${name}" 유튜브`, `"${name}" 페이스북`, `"${name}" 블로그`, `${name} 강남언니`, `${name} 네이버 플레이스`, + `site:instagram.com ${core}`, `site:youtube.com ${name}`, `site:facebook.com ${name}`, +]; +for (const q of queries) { searchLog.push(q); const res = await fcSearch(q, 8); for (const r of res) { searchHits.push({ ...r, query: q }); addCandidate(r.url, 'search', q); extractAll(r.snippet, 'search:snippet', q); } await sleep(300); } +for (const q of [`${name} 인스타그램`, `${name} 유튜브`, `${name} 페이스북`, `${name} 블로그`]) { searchLog.push('naver:' + q); for (const r of await naverSearch('webkr', q, 10)) { searchHits.push({ ...r, query: 'naver:' + q }); addCandidate(r.url, 'search', q); } } +for (const r of await naverSearch('blog', name, 10)) { searchHits.push({ ...r, query: 'naver:blog' }); addCandidate(r.url, 'search:naverblog', name); } +if (KEYS.yt) { + for (const q of [name, `${name} 성형외과`]) { + try { const r = await fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&type=channel&q=${encodeURIComponent(q)}&maxResults=5&key=${KEYS.yt}`); const j = await r.json(); for (const it of j.items ?? []) { addCandidate(`https://www.youtube.com/channel/${it.id.channelId}`, 'youtube:search', q); searchHits.push({ url: `https://www.youtube.com/channel/${it.id.channelId}`, title: it.snippet.title, snippet: it.snippet.description, query: 'yt:' + q }); } } catch { /* */ } + } +} + +// ---------- ③ 핸들 변형 (존재 확인만) ---------- +const variantsTried = []; +if (domainBase && domainBase.length >= 3) { + const SUF = ['', 'official', 'ps', 'clinic', 'plasticsurgery', 'kr', 'korea', 'seoul']; + const SEP = { instagram: ['', '.', '_'], youtube: ['', '_', '-'], tiktok: ['', '.', '_'] }; + const vs = new Set(); for (const s of SUF) for (const sep of ['', '.', '_', '-']) vs.add(s ? `${domainBase}${sep}${s}` : domainBase); + for (const p of ['instagram', 'youtube']) { + for (const h of [...vs].filter((h) => SEP[p].some((sep) => !h.includes('.') && !h.includes('_') && !h.includes('-') || h.includes(sep)))) { + if (cands[p].has(p === 'youtube' ? '@' + h : h)) continue; + variantsTried.push(`${p}:${h}`); + if (p === 'youtube' && KEYS.yt) { try { const r = await fetch(`https://www.googleapis.com/youtube/v3/channels?part=id&forHandle=${encodeURIComponent(h)}&key=${KEYS.yt}`); const j = await r.json(); if (j.items?.length) addCandidate(`https://www.youtube.com/channel/${j.items[0].id}`, 'variant', h); } catch { /* */ } } + if (p === 'instagram') { const r = await fetchText(`https://www.instagram.com/${h}/`, 0); if (r.status === 200 && !/Page Not Found|페이지를 찾을 수 없|login/i.test(r.body.slice(0, 3000))) addCandidate(`https://www.instagram.com/${h}/`, 'variant', h); await sleep(400); } + if (variantsTried.length > 40) break; + } + } +} + +// ---------- 검증 ---------- +const ytChannels = new Map(); +async function ytInfo(key) { + if (!KEYS.yt) return null; + const param = key.startsWith('UC') ? `id=${key}` : `forHandle=${encodeURIComponent(key.slice(1))}`; + try { const r = await fetch(`https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&${param}&key=${KEYS.yt}`); const j = await r.json(); const c = j.items?.[0]; if (!c) return { exists: false }; return { exists: true, id: c.id, title: c.snippet.title, description: c.snippet.description ?? '', handle: c.snippet.customUrl, subscribers: Number(c.statistics.subscriberCount ?? 0), videos: Number(c.statistics.videoCount ?? 0), views: Number(c.statistics.viewCount ?? 0) }; } catch { return null; } +} +async function profileInfo(p, c) { + if (p === 'youtube') return ytInfo(c.key); + if (p === 'naverBlog') { const r = await fetchText(`https://rss.blog.naver.com/${c.key}.xml`, 0); if (r.status === 404) return { exists: false }; if (!r.body) return null; return { exists: true, title: r.body.match(/([^<]*)<\/title>/)?.[1] ?? '', description: r.body.match(/<description>([^<]*)<\/description>/)?.[1] ?? '' }; } + const r = await fetchText(c.url, 0); + if (r.status === 404) return { exists: false }; + if (!r.body || r.status !== 200) return null; // 차단·로그인 요구 + const og = (prop) => r.body.match(new RegExp(`<meta[^>]+(?:property|name)="${prop}"[^>]+content="([^"]*)"`, 'i'))?.[1] ?? ''; + const title = og('og:title') || r.body.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] || ''; + if (/login|로그인|Page Not Found|콘텐츠를 사용할 수 없|isn't available/i.test(title) && !title.includes(core)) return null; + return { exists: true, title, description: og('og:description') || og('description') }; +} +const norm = (s) => String(s ?? '').replace(/\s/g, '').toLowerCase(); +const domainHost = home ? new URL(home).host.replace(/^www\./, '') : ''; +const results = {}; +for (const [p, map] of Object.entries(cands)) { + results[p] = []; + for (const c of map.values()) { + const info = await profileInfo(p, c).catch(() => null); + const ev = []; let score = 0; + const hasSite = [...c.sources].some((s) => s.startsWith('site')); + const hostPath = c.url.replace(/^https?:\/\/(www\.|m\.)?/, '').replace(/\/$/, '').toLowerCase(); + // 역링크는 경로 경계까지 맞아야 한다 (instagram.com/wonjin 이 instagram.com/wonjin_official 안에 들어 있는 것은 역링크가 아니다) + const reciprocal = hasSite || new RegExp(hostPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?![a-z0-9._-])').test(homeReciprocal); + if (reciprocal) { score += 3; ev.push('홈페이지에서 역링크'); } + if (c.sources.has('registry')) { score += 2; ev.push('레지스트리 등재'); } + if (info?.exists) { + const t = norm(info.title), d = norm(info.description); + if (t.includes(norm(core)) || (domainBase && t.includes(domainBase))) { const w = ['gangnamunni', 'babitalk', 'naverPlace'].includes(p) ? 3 : 2; score += w; ev.push(`프로필 제목에 상호: "${String(info.title).slice(0, 40)}"`); } + if (domainHost && d.includes(domainHost.toLowerCase())) { score += 2; ev.push('프로필 설명에 홈페이지 도메인'); } + else if (phone && d.replace(/[-.]/g, '').includes(phone.replace(/-/g, ''))) { score += 2; ev.push('프로필 설명에 대표 전화'); } + } + const snip = searchHits.filter((h) => norm(h.url).includes(norm(c.key)) && (norm(h.title + h.snippet).includes(norm(core)))); + if (snip.length && !ev.some((e) => e.startsWith('프로필 제목'))) { score += 1; ev.push(`검색 결과 제목/요약에 상호 (${snip[0].query})`); } + let status = info === null && !hasSite ? 'BLOCKED' : info && info.exists === false ? 'HANDLE_DEAD' : score >= 3 ? 'CONFIRMED' : score >= 1 ? 'CANDIDATE' : 'UNRELATED'; + if (status === 'BLOCKED' && score >= 3) status = 'CONFIRMED'; + results[p].push({ platform: p, key: c.key, url: c.url, status, score, evidence: ev, sources: [...c.sources], ...(info?.exists ? { title: info.title, handle: info.handle, channelId: info.id, subscribers: info.subscribers, videos: info.videos, views: info.views } : {}) }); + await sleep(150); + } + if (p === 'youtube') { const byId = new Map(); for (const x of results[p]) { const id = x.channelId ?? x.key; const prev = byId.get(id); if (!prev) byId.set(id, x); else { prev.score = Math.max(prev.score, x.score); prev.evidence = [...new Set([...prev.evidence, ...x.evidence])]; prev.sources = [...new Set([...prev.sources, ...x.sources])]; if (x.status === 'CONFIRMED') prev.status = 'CONFIRMED'; } } results[p] = [...byId.values()]; } + results[p].sort((a, b) => b.score - a.score || (b.subscribers ?? 0) - (a.subscribers ?? 0)); +} +const allBranches = Boolean(home) && Boolean(KEYS.fc) && Boolean(KEYS.nid) && Boolean(KEYS.yt); +const inventory = Object.entries(results).map(([p, list]) => { + const confirmed = list.filter((x) => x.status === 'CONFIRMED'); + const state = confirmed.length ? 'CONFIRMED' : list.some((x) => x.status === 'CANDIDATE') ? 'CANDIDATE' : list.some((x) => x.status === 'BLOCKED') ? 'BLOCKED' : list.length ? 'HANDLE_DEAD' : allBranches ? 'NOT_FOUND' : 'UNVERIFIED'; + return { platform: p, state, confirmed: confirmed.length, candidates: list.filter((x) => x.status === 'CANDIDATE').length, tried: `사이트 ${sitePages.length}면 · 검색 ${searchLog.length}건 · 변형 ${variantsTried.filter((v) => v.startsWith(p)).length}` }; +}); +const out = { clinic, name, url: home ?? null, checkedAt: today, phone: phone || null, branches: { sitePages, searchQueries: searchLog, variantsTried, registry: Boolean(registry), allBranches }, inventory, platforms: results, rules: 'datacrawling 스킬 §1~§2. CONFIRMED=근거 점수 3 이상, CANDIDATE=사람 확인, NOT_FOUND 는 다섯 갈래 전부 실행 시에만' }; +writeFileSync(OUT, JSON.stringify(out, null, 1)); +for (const row of inventory) log(` ${row.platform.padEnd(11)} ${row.state.padEnd(11)} 확정 ${row.confirmed} 후보 ${row.candidates} ${results[row.platform].filter((x) => x.status === 'CONFIRMED').map((x) => x.key + (x.subscribers ? `(${x.subscribers})` : '')).join(', ')}`); +log(`저장 ${OUT}`); diff --git a/workers/supporters-build/README.md b/workers/supporters-build/README.md index d8d3083..12abae0 100644 --- a/workers/supporters-build/README.md +++ b/workers/supporters-build/README.md @@ -3,7 +3,8 @@ 병원 홈페이지 URL 하나로 근거 수집 → 팩트 시트·의료진 초안 → 첫 배치 기획 → 글 생성(GPT-4.1, 게이트·근거 대조) → Astro 빌드·발행 게이트 → Vercel 미리보기(noindex) 까지 돈다. 사람 게이트(의학 검토·게시 승인)는 자동화하지 않는다. ``` -node workers/supporters-build/run.mjs --clinic <id> --url <https://...> [--name 상호] [--deploy] [--build-id <uuid>] [--max-cost-usd 10] +node workers/supporters-build/run.mjs --clinic <id> --url <https://...> [--name 상호] [--logo file.svg] [--youtube 채널] [--deploy] [--build-id <uuid>] [--max-cost-usd 10] +# 단계: evidence → data → discover(채널 발견·엔티티 검증) → youtube(로고·파비콘·채널 집계) → images(의료법 기준 이미지) → inputs → briefs → generate → build → deploy node workers/supporters-build/poll.mjs [--loop 300] [--no-deploy] # supporter_builds 큐(status=queued)를 집어 run.mjs 실행 ``` diff --git a/workers/supporters-build/run.mjs b/workers/supporters-build/run.mjs index b8c9530..3d3e0db 100644 --- a/workers/supporters-build/run.mjs +++ b/workers/supporters-build/run.mjs @@ -26,7 +26,7 @@ const clinic = opt('clinic'); const url = opt('url'); if (!clinic || !url) { console.error('사용법: --clinic <id> --url <https://...>'); process.exit(2); } const WORK = resolve(opt('work', join(homedir(), 'supporters-builds', clinic))); const today = new Date().toISOString().slice(0, 10); -const PHASES = ['evidence', 'data', 'youtube', 'images', 'inputs', 'briefs', 'generate', 'build', 'deploy', 'done']; +const PHASES = ['evidence', 'data', 'discover', 'youtube', 'images', 'inputs', 'briefs', 'generate', 'build', 'deploy', 'done']; const startFrom = opt('from', 'evidence'); (function loadEnv() { const p = join(ROOT, '.env'); if (!existsSync(p)) return; for (const line of readFileSync(p, 'utf8').split('\n')) { const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, ''); } })(); @@ -121,7 +121,31 @@ await phase('data', () => { return `${fact.shortName || '(상호 확인 대기)'} · 전화 ${fact.phone || '확인 대기'} · 의료진 ${doctors.length}명`; }); -// 2b. 로고·파비콘 (--logo <svg|png 파일>) + 유튜브 채널 집계 (--youtube <채널> 또는 근거의 유튜브 링크 자동 발견) +// 2a. 채널 발견 + 엔티티 검증 (datacrawling 스킬). 상호·URL 로 유튜브·인스타·페북·블로그·틱톡·카카오·강남언니·플레이스를 찾고, CONFIRMED 만 사이트에 반영한다. CANDIDATE 는 사람 확인 목록. +await phase('discover', () => { + if (flag('no-discover')) return '건너뜀 (--no-discover)'; + const fact = readJson(join(DATA, 'factSheet.json')); + const argv = ['scripts/discover_channels.mjs', '--clinic', clinic, '--url', url, '--evidence', EV, '--out', join(WORK, 'channels.json')]; + if (opt('name') || fact.shortName) argv.push('--name', opt('name') || fact.shortName); + const r = run('node', argv); + if (!r.ok || !existsSync(join(WORK, 'channels.json'))) throw new Error(`채널 탐색 실패\n${tail(r.out)}`); + const ch = readJson(join(WORK, 'channels.json')); + cpSync(join(WORK, 'channels.json'), join(DATA, 'channels.json')); + // CONFIRMED 첫 항목(구독자·점수 순)을 팩트 시트 surfaces 에. 유튜브는 youtube 단계가 이 값을 쓴다. + const top = (p) => (ch.platforms[p] ?? []).find((x) => x.status === 'CONFIRMED'); + const LABEL = { youtube: '유튜브', instagram: '인스타그램', facebook: '페이스북', naverBlog: '네이버 블로그', tiktok: '틱톡', kakao: '카카오톡 채널', gangnamunni: '강남언니', naverPlace: '네이버 플레이스', babitalk: '바비톡' }; + fact.surfaces = fact.surfaces ?? {}; + for (const p of Object.keys(LABEL)) { + const x = top(p); if (!x) continue; + fact.surfaces[p] = { ...(fact.surfaces[p] ?? {}), label: LABEL[p], url: x.url, title: x.title ?? undefined, checked: ch.checkedAt, ...(x.subscribers ? { subscribers: x.subscribers, videos: x.videos } : {}) }; + } + if (!fact.kakao && top('kakao')) fact.kakao = top('kakao').url; + writeJson(join(DATA, 'factSheet.json'), fact); + status.report = { ...(status.report ?? {}), channels: { inventory: ch.inventory, confirmed: Object.fromEntries(Object.entries(ch.platforms).map(([p, l]) => [p, l.filter((x) => x.status === 'CONFIRMED').map((x) => ({ key: x.key, url: x.url, title: x.title, subscribers: x.subscribers, evidence: x.evidence }))])), candidates: Object.fromEntries(Object.entries(ch.platforms).map(([p, l]) => [p, l.filter((x) => x.status === 'CANDIDATE').map((x) => ({ key: x.key, url: x.url, title: x.title, evidence: x.evidence }))])) } }; + return ch.inventory.map((i) => `${i.platform} ${i.state}${i.confirmed ? `(${i.confirmed})` : ''}`).join(' · '); +}); + +// 2b. 로고·파비콘 (--logo <svg|png 파일>) + 유튜브 채널 집계 (--youtube <채널> 또는 발견 단계의 CONFIRMED 유튜브) await phase('youtube', () => { const notes = []; // 로고: 헤더·푸터·카드 대체 이미지·파비콘. SVG 는 viewBox 를 정사각 파비콘으로 감싼다. @@ -153,6 +177,11 @@ await phase('youtube', () => { // 유튜브: 채널 인자 없으면 근거 페이지 링크에서 youtube.com/channel|@handle 을 찾는다 let channel = opt('youtube') ?? null; + if (!channel && existsSync(join(DATA, 'channels.json'))) { + // 발견 단계에서 CONFIRMED 된 유튜브 중 구독자가 가장 많은 채널 + const yt = (readJson(join(DATA, 'channels.json')).platforms?.youtube ?? []).filter((x) => x.status === 'CONFIRMED').sort((a, b) => (b.subscribers ?? 0) - (a.subscribers ?? 0))[0]; + if (yt) { channel = yt.channelId ?? yt.key; notes.push(`유튜브 채널 자동 선택: ${yt.title ?? yt.key}`); } + } if (!channel) { const idx = readJson(join(EV, 'index.json')); outer: for (const p of idx.pages.slice(0, 30)) {