From b7836cdee95aa7973eb465e8097b7279bd6451bf Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Fri, 4 Sep 2026 17:52:02 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9C=A0=ED=86=B5=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C=EC=99=80=20=ED=8E=98=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EB=B6=81=20=EC=88=98=EC=A7=91=20=EC=9E=AC=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/social/FacebookPageFeedClient.java | 22 +++++++++++++++++-- .../templates/distribution-feed.html | 2 +- src/main/resources/templates/layout/base.html | 4 +++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClient.java b/src/main/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClient.java index 67c67cf..543eb18 100644 --- a/src/main/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClient.java +++ b/src/main/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClient.java @@ -26,6 +26,9 @@ import java.util.regex.Pattern; public class FacebookPageFeedClient implements SocialFeedClient { private static final String PAGE_PLUGIN_APP_ID = "776730922422337"; private static final Pattern REEL_ID = Pattern.compile("/(?:reel|videos)/(\\d+)"); + /** Facebook가 데이터센터별로 빈 피드를 돌려주는 경우가 있어 공식 로케일 호스트를 순서대로 시도한다. */ + private static final List RENDERER_HOSTS = List.of( + "www.facebook.com", "ko-kr.facebook.com", "en-gb.facebook.com"); private final RestTemplate restTemplate; private final ObjectMapper objectMapper; @@ -40,6 +43,21 @@ public class FacebookPageFeedClient implements SocialFeedClient { @Override public SocialFeedBatch fetch(SocialChannel channel) { + IllegalStateException lastFailure = null; + for (String host : RENDERER_HOSTS) { + try { + SocialFeedBatch batch = fetchFromHost(channel, host); + if (!batch.videos().isEmpty()) return batch; + lastFailure = new IllegalStateException("Facebook 공개 피드에 릴이 없습니다: " + host); + } catch (IllegalStateException e) { + lastFailure = e; + } + } + throw new IllegalStateException("Facebook 공개 피드를 읽지 못했습니다: " + + (lastFailure == null ? "빈 응답" : friendly(lastFailure)), lastFailure); + } + + private SocialFeedBatch fetchFromHost(SocialChannel channel, String host) { try { Map config = new LinkedHashMap<>(); config.put("app_id", PAGE_PLUGIN_APP_ID); @@ -56,7 +74,7 @@ public class FacebookPageFeedClient implements SocialFeedClient { config.put("is_xfbml", false); config.put("referer_uri", ""); - URI uri = UriComponentsBuilder.fromHttpUrl("https://www.facebook.com/platform/plugin/tab/renderer/") + URI uri = UriComponentsBuilder.fromHttpUrl("https://" + host + "/platform/plugin/tab/renderer/") .queryParam("key", "timeline") .queryParam("config_json", objectMapper.writeValueAsString(config)) .queryParam("__a", "1") @@ -73,7 +91,7 @@ public class FacebookPageFeedClient implements SocialFeedClient { return parseResponse(response.getBody(), Math.max(1, maxItems)); } catch (Exception e) { if (e instanceof IllegalStateException state) throw state; - throw new IllegalStateException("Facebook 공개 피드를 읽지 못했습니다: " + friendly(e), e); + throw new IllegalStateException(host + " Facebook 피드 오류: " + friendly(e), e); } } diff --git a/src/main/resources/templates/distribution-feed.html b/src/main/resources/templates/distribution-feed.html index 73dcca2..28a9b78 100644 --- a/src/main/resources/templates/distribution-feed.html +++ b/src/main/resources/templates/distribution-feed.html @@ -57,7 +57,7 @@ function date(iso){return iso?new Date(iso).toLocaleString('ko-KR',{month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}):'게시 시각 미상'} async function loadChannels(){const box=document.getElementById('channelList');try{const list=await api(`${API}/channels?topic=DISTRIBUTION`);box.innerHTML=list.length?list.map(c=>``).join(''):''}catch(e){box.innerHTML=``}} async function loadVideos(){const grid=document.getElementById('videoGrid'),empty=document.getElementById('emptyState');try{const list=await api(`${API}/videos?topic=DISTRIBUTION`);document.getElementById('videoCount').textContent=`${list.length}건`;empty.classList.toggle('hidden',list.length>0);grid.innerHTML=list.map(v=>``).join('');if(window.lucide)lucide.createIcons()}catch(e){toast('영상을 불러오지 못했습니다: '+e.message,true)}} - async function addChannel(){const input=document.getElementById('channelUrl'),button=document.getElementById('addBtn'),url=input.value.trim();if(!url){input.focus();return}button.disabled=true;try{const c=await api(`${API}/channels`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,topic:'DISTRIBUTION',notificationEnabled:document.getElementById('notifyEnabled').checked})});input.value='';toast(`${c.title} 채널을 등록했습니다`);await loadChannels()}catch(e){toast('등록 실패: '+e.message,true)}finally{button.disabled=false}} + async function addChannel(){const input=document.getElementById('channelUrl'),button=document.getElementById('addBtn'),url=input.value.trim();if(!url){input.focus();return}button.disabled=true;try{const c=await api(`${API}/channels`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,topic:'DISTRIBUTION',notificationEnabled:document.getElementById('notifyEnabled').checked})});input.value='';toast(`${c.title} 채널을 등록했습니다 · 첫 수집을 시작합니다`);await loadChannels();await collectNow()}catch(e){toast('등록 실패: '+e.message,true)}finally{button.disabled=false}} async function removeChannel(id){if(!confirm('이 채널과 수집 영상을 삭제할까요?'))return;try{await api(`${API}/channels/${id}`,{method:'DELETE'});toast('채널을 삭제했습니다');await Promise.all([loadChannels(),loadVideos()])}catch(e){toast('삭제 실패: '+e.message,true)}} async function collectNow(){const button=document.getElementById('collectBtn'),original=button.innerHTML;button.disabled=true;button.textContent='수집 중…';try{const r=await api(`${API}/collect?topic=DISTRIBUTION`,{method:'POST'});toast(`수집 완료 · 새 영상 ${r.savedVideos}건 · 알림 ${r.alerted}건${r.failed?` · 실패 ${r.failed}개`:''}`,r.failed>0);await Promise.all([loadChannels(),loadVideos()])}catch(e){toast('수집 실패: '+e.message,true)}finally{button.disabled=false;button.innerHTML=original;if(window.lucide)lucide.createIcons()}} Promise.all([loadChannels(),loadVideos()]); diff --git a/src/main/resources/templates/layout/base.html b/src/main/resources/templates/layout/base.html index 1629e05..46272f0 100644 --- a/src/main/resources/templates/layout/base.html +++ b/src/main/resources/templates/layout/base.html @@ -24,6 +24,7 @@ + @@ -53,6 +54,7 @@ lucide.createIcons(); + - \ No newline at end of file +