- 위험시설: 석유화학단지(5), LNG기지(10), 유류탱크(15), 위험물항만(6) 추가 - 에너지/발전시설: 원자력(5), 화력(5) 추가; 발전/변전·풍력단지 그룹 이동 - 산업공정/제조시설: 조선소(6), 폐수처리(5), 시멘트/제철소(5) 추가 - 위험/산업 인프라 수퍼그룹 신설 (3단계 계층 구조) - LayerPanel: 레이어 수량을 우측 숫자 뱃지로 표시 (괄호 제거) - 해외시설 하위항목: 이란탭=호르무즈 10개국, 한국탭=중국·일본 - EventLog: 재난/안전뉴스 섹션 추가 (한국탭), OSINT 접기/펼치기 - OSINT 뉴스 2026-03-21 기준으로 업데이트 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
948 lines
36 KiB
TypeScript
948 lines
36 KiB
TypeScript
// ═══ Real-time OSINT Feed Service ═══
|
|
// Fetches live news from GDELT Project API + Google News RSS
|
|
// Focus: Iran, Hormuz Strait, Middle East military, oil/energy
|
|
|
|
export interface OsintItem {
|
|
id: string;
|
|
timestamp: number; // unix ms
|
|
title: string;
|
|
source: string; // "Reuters", "AP", etc.
|
|
url: string;
|
|
category: 'military' | 'oil' | 'diplomacy' | 'shipping' | 'nuclear' | 'general' | 'maritime_accident' | 'fishing' | 'maritime_traffic';
|
|
language: 'en' | 'ko' | 'other';
|
|
imageUrl?: string;
|
|
lat?: number; // extracted from title keywords
|
|
lng?: number;
|
|
}
|
|
|
|
// ── Korean maritime location extraction ──
|
|
const KOREA_LOCATIONS: [RegExp, number, number][] = [
|
|
// 주요 항구/해역
|
|
[/인천|경인/i, 37.45, 126.60],
|
|
[/평택|당진/i, 36.97, 126.83],
|
|
[/대산/i, 36.98, 126.35],
|
|
[/군산/i, 35.97, 126.65],
|
|
[/목포/i, 34.79, 126.38],
|
|
[/완도/i, 34.31, 126.76],
|
|
[/여수|광양/i, 34.74, 127.74],
|
|
[/통영/i, 34.85, 128.43],
|
|
[/거제/i, 34.88, 128.62],
|
|
[/마산|창원/i, 35.08, 128.60],
|
|
[/부산/i, 35.10, 129.04],
|
|
[/울산/i, 35.51, 129.39],
|
|
[/포항/i, 36.03, 129.37],
|
|
[/울릉/i, 37.48, 130.91],
|
|
[/독도/i, 37.24, 131.87],
|
|
[/묵호|동해시/i, 37.55, 129.11],
|
|
[/강릉/i, 37.75, 128.90],
|
|
[/속초/i, 38.20, 128.59],
|
|
[/제주/i, 33.51, 126.53],
|
|
[/서귀포/i, 33.24, 126.56],
|
|
[/마라도/i, 33.11, 126.27],
|
|
[/추자/i, 33.95, 126.30],
|
|
[/흑산/i, 34.68, 125.44],
|
|
[/백령/i, 37.97, 124.72],
|
|
[/연평/i, 37.67, 125.70],
|
|
[/대청/i, 37.83, 124.72],
|
|
[/진도/i, 34.38, 126.31],
|
|
[/태안/i, 36.75, 126.30],
|
|
[/보령/i, 36.35, 126.59],
|
|
[/서해|서쪽.*바다/i, 36.50, 125.50],
|
|
[/남해|남쪽.*바다/i, 34.50, 128.00],
|
|
[/동해|동쪽.*바다/i, 37.00, 130.00],
|
|
[/대한해협/i, 34.00, 128.50],
|
|
[/제주해협/i, 33.50, 126.80],
|
|
[/호르무즈|Hormuz/i, 26.56, 56.25],
|
|
[/페르시아만|Persian Gulf/i, 27.00, 51.50],
|
|
];
|
|
|
|
/** 제목에서 위치 추출 */
|
|
export function extractLocation(title: string): { lat: number; lng: number } | null {
|
|
for (const [pattern, lat, lng] of KOREA_LOCATIONS) {
|
|
if (pattern.test(title)) return { lat, lng };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── Category classification by keywords ──
|
|
const CATEGORY_RULES: [RegExp, OsintItem['category']][] = [
|
|
// Maritime-specific (must come before general shipping)
|
|
[/해양사고|해난|좌초|침몰|전복|충돌사고|구조작업|해상사고|조난|실종.*선|표류|구명|인명사고|maritime accident|collision.*vessel|capsiz|grounding|sinking|rescue.*sea|distress/i, 'maritime_accident'],
|
|
[/어선|어업|어획|수산|조업|불법조업|중국어선|오징어|꽃게|해삼|어민|fishing|trawler|illegal fishing|IUU|poaching|fishery|fish.*boat/i, 'fishing'],
|
|
[/해상교통|VTS|항로|선박통항|교통관제|해상안전|입출항|항만.*교통|해사|AIS.*추적|해양수산부|해양경찰청|해경|vessel traffic|sea lane|port congestion|maritime traffic|shipping lane|navigation.*safety|Ministry of Oceans|Korea Coast Guard/i, 'maritime_traffic'],
|
|
[/\b(strike|missile|attack|bomb|military|war|defense|weapon|drone|fighter|navy|army|air\s?force|IRGC|pentagon|carrier|destroyer|intercept|airstrike|combat|troop)\b/i, 'military'],
|
|
[/\b(oil|crude|WTI|brent|OPEC|barrel|petroleum|gas|LNG|energy|refiner|pipeline|tanker|fuel)\b/i, 'oil'],
|
|
[/\b(diplomacy|sanction|UN|NATO|treaty|negotiat|summit|ambassador|ceasefire|peace|talk|deal|accord)\b/i, 'diplomacy'],
|
|
[/\b(ship|vessel|maritime|strait|hormuz|port|cargo|shipping|blockade|naval|fleet|escort|piracy|AIS)\b/i, 'shipping'],
|
|
[/\b(nuclear|uranium|centrifuge|enrichment|IAEA|nonproliferation|warhead|plutonium)\b/i, 'nuclear'],
|
|
];
|
|
|
|
function classifyArticle(title: string): OsintItem['category'] {
|
|
for (const [pattern, cat] of CATEGORY_RULES) {
|
|
if (pattern.test(title)) return cat;
|
|
}
|
|
return 'general';
|
|
}
|
|
|
|
// ── GDELT DOC 2.0 API ──
|
|
// Free, no auth, returns JSON with articles matching keywords
|
|
// 이란 상황: 호르무즈 해협 중심
|
|
const GDELT_KEYWORDS_IRAN = '"Strait of Hormuz" OR Hormuz OR "Persian Gulf" OR Iran OR IRGC OR "oil tanker" OR "Gulf shipping" OR "oil price" OR "Middle East oil"';
|
|
// 한국 현황: 해양사고/해상교통/어선/수산/항만/해상안전
|
|
const GDELT_KEYWORDS_KOREA = '"Korea coast guard" OR "Korea maritime accident" OR "Korea ship collision" OR "Korea vessel rescue" OR "Korea illegal fishing" OR "Korea NLL" OR "Korea Dokdo" OR "Korea sea patrol" OR "Korea ship sinking"';
|
|
|
|
const GNEWS_KR_IRAN = '호르무즈 해협 OR 이란 유조선 OR 페르시아만 OR 중동 원유 OR 호르무즈 봉쇄 OR 이란 해군';
|
|
const GNEWS_KR_KOREA = '해양사고 OR 해상구조 OR 해양경찰 OR 해양경찰청 OR 선박충돌 OR 선박좌초 OR 해상안전 OR 불법조업 OR 해상교통관제 OR 선박침몰 OR 해경 단속 OR NLL OR 독도 영해';
|
|
|
|
const GNEWS_EN_IRAN = '"Strait of Hormuz" OR "Persian Gulf" OR "Iran oil" OR "Gulf tanker" OR "Iran navy" OR "Hormuz blockade"';
|
|
const GNEWS_EN_KOREA = '"Korea maritime accident" OR "Korea fishing" OR "Korea port" OR "Korea coast guard" OR "Korea vessel traffic" OR "Korea sea safety"';
|
|
|
|
async function fetchGDELT(keywords: string): Promise<OsintItem[]> {
|
|
const query = encodeURIComponent(keywords);
|
|
const url = `/api/gdelt/api/v2/doc/doc?query=${query}&mode=ArtList&maxrecords=30&format=json&sort=DateDesc×pan=24h`;
|
|
|
|
try {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`GDELT ${res.status}`);
|
|
const data = await res.json();
|
|
|
|
if (!data.articles || !Array.isArray(data.articles)) return [];
|
|
|
|
return data.articles.map((a: Record<string, string>, i: number) => {
|
|
const title = a.title || 'Untitled';
|
|
const loc = extractLocation(title);
|
|
return {
|
|
id: `gdelt-${i}-${a.seendate || Date.now()}`,
|
|
timestamp: a.seendate ? new Date(a.seendate.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z')).getTime() : Date.now(),
|
|
title,
|
|
source: a.domain || a.sourcecountry || 'Unknown',
|
|
url: a.url || '',
|
|
category: classifyArticle(title),
|
|
language: (a.language === 'Korean' ? 'ko' : a.language === 'English' ? 'en' : 'other') as OsintItem['language'],
|
|
imageUrl: a.socialimage || undefined,
|
|
...(loc ? { lat: loc.lat, lng: loc.lng } : {}),
|
|
};
|
|
});
|
|
} catch (err) {
|
|
console.warn('GDELT fetch failed:', err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ── Google News RSS (Korean) ──
|
|
async function fetchGoogleNewsKR(keywords: string): Promise<OsintItem[]> {
|
|
const query = encodeURIComponent(keywords);
|
|
const url = `/api/rss/rss/search?q=${query}&hl=ko&gl=KR&ceid=KR:ko`;
|
|
|
|
try {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`Google RSS ${res.status}`);
|
|
const text = await res.text();
|
|
|
|
const parser = new DOMParser();
|
|
const xml = parser.parseFromString(text, 'text/xml');
|
|
const items = xml.querySelectorAll('item');
|
|
|
|
const results: OsintItem[] = [];
|
|
items.forEach((item, i) => {
|
|
const title = item.querySelector('title')?.textContent || '';
|
|
const link = item.querySelector('link')?.textContent || '';
|
|
const pubDate = item.querySelector('pubDate')?.textContent || '';
|
|
const source = item.querySelector('source')?.textContent || '';
|
|
|
|
const loc = extractLocation(title);
|
|
results.push({
|
|
id: `gnews-kr-${i}-${Date.now()}`,
|
|
timestamp: pubDate ? new Date(pubDate).getTime() : Date.now(),
|
|
title,
|
|
source: source || 'Google News',
|
|
url: link,
|
|
category: classifyArticle(title),
|
|
language: 'ko',
|
|
...(loc ? { lat: loc.lat, lng: loc.lng } : {}),
|
|
});
|
|
});
|
|
|
|
return results;
|
|
} catch (err) {
|
|
console.warn('Google News KR fetch failed:', err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ── Google News RSS (English) ──
|
|
async function fetchGoogleNewsEN(keywords: string): Promise<OsintItem[]> {
|
|
const query = encodeURIComponent(keywords);
|
|
const url = `/api/rss/rss/search?q=${query}&hl=en&gl=US&ceid=US:en`;
|
|
|
|
try {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`Google RSS EN ${res.status}`);
|
|
const text = await res.text();
|
|
|
|
const parser = new DOMParser();
|
|
const xml = parser.parseFromString(text, 'text/xml');
|
|
const items = xml.querySelectorAll('item');
|
|
|
|
const results: OsintItem[] = [];
|
|
items.forEach((item, i) => {
|
|
const title = item.querySelector('title')?.textContent || '';
|
|
const link = item.querySelector('link')?.textContent || '';
|
|
const pubDate = item.querySelector('pubDate')?.textContent || '';
|
|
const source = item.querySelector('source')?.textContent || '';
|
|
|
|
const loc = extractLocation(title);
|
|
results.push({
|
|
id: `gnews-en-${i}-${Date.now()}`,
|
|
timestamp: pubDate ? new Date(pubDate).getTime() : Date.now(),
|
|
title,
|
|
source: source || 'Google News',
|
|
url: link,
|
|
category: classifyArticle(title),
|
|
language: 'en',
|
|
...(loc ? { lat: loc.lat, lng: loc.lng } : {}),
|
|
});
|
|
});
|
|
|
|
return results;
|
|
} catch (err) {
|
|
console.warn('Google News EN fetch failed:', err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ── X.com (Twitter) — U.S. Central Command (@CENTCOM) RSS ──
|
|
// 여러 Nitter 인스턴스 + RSSHub fallback
|
|
// 중동 지역 위치 패턴 (CENTCOM 포스트용)
|
|
const ME_LOCATIONS: [RegExp, number, number][] = [
|
|
[/Iran|Tehran|이란|테헤란/i, 35.69, 51.39],
|
|
[/Hormuz|호르무즈/i, 26.56, 56.25],
|
|
[/Iraq|Baghdad|이라크|바그다드/i, 33.31, 44.37],
|
|
[/Syria|시리아|Damascus/i, 33.51, 36.28],
|
|
[/Yemen|Houthi|예멘|후티|Sanaa/i, 15.37, 44.19],
|
|
[/Bahrain|바레인/i, 26.22, 50.60],
|
|
[/Qatar|카타르|Al Udeid/i, 25.12, 51.32],
|
|
[/UAE|Abu Dhabi|아부다비|Dubai/i, 24.25, 54.55],
|
|
[/Kuwait|쿠웨이트/i, 29.35, 47.52],
|
|
[/Erbil|에르빌/i, 36.19, 44.01],
|
|
[/Lebanon|Hezbollah|레바논|헤즈볼라/i, 33.85, 35.86],
|
|
[/Red Sea|홍해/i, 20.00, 38.00],
|
|
[/Gulf of Oman|오만만/i, 24.50, 58.50],
|
|
[/Bandar Abbas|반다르아바스/i, 27.18, 56.28],
|
|
[/Natanz|나탄즈/i, 33.72, 51.73],
|
|
[/Isfahan|이스파한/i, 32.65, 51.67],
|
|
];
|
|
|
|
function extractMELocation(text: string): { lat: number; lng: number } | null {
|
|
for (const [pattern, lat, lng] of ME_LOCATIONS) {
|
|
if (pattern.test(text)) return { lat, lng };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── CENTCOM 최신 게시물 (수동 업데이트 — RSS 대체) ──
|
|
// Nitter/RSSHub 모두 X.com 차단으로 사용 불가하므로 주요 CENTCOM 게시물 수동 관리
|
|
const CENTCOM_POSTS: { text: string; date: string; url: string }[] = [
|
|
// ── 3월 21일 (D+21) 최신 ──
|
|
{
|
|
text: 'CENTCOM: US-Iran ceasefire negotiations in Muscat enter Day 2. CENTCOM forces maintaining "minimal operations" posture pending diplomatic outcome',
|
|
date: '2026-03-21T06:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'UPDATE: Strait of Hormuz commercial traffic restored to 72% of pre-conflict levels. 23 tankers transited safely in past 24hrs under coalition escort',
|
|
date: '2026-03-21T02:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 20일 (D+20) ──
|
|
{
|
|
text: 'CENTCOM: US and Iranian delegations meet in Muscat, Oman for preliminary ceasefire talks. Omani FM Al-Busaidi mediating. No agreement yet but "atmosphere constructive"',
|
|
date: '2026-03-20T14:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'Brent crude falls to $97/barrel on ceasefire talk optimism — first time below $100 since Operation Epic Fury began',
|
|
date: '2026-03-20T08:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'Multiple senior IRGC commanders reported to have departed Iran for Russia. CENTCOM assesses Iran\'s strategic command continuity "severely degraded"',
|
|
date: '2026-03-20T04:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 19일 (D+19) ──
|
|
{
|
|
text: 'BREAKING: Iran signals readiness for "unconditional ceasefire talks" through Oman channel. CENTCOM suspends offensive air operations pending diplomatic contact',
|
|
date: '2026-03-19T18:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'CENTCOM: Strait of Hormuz now 60% restored to normal commercial traffic. Coalition minesweeping teams cleared 41 mines total since Day 1',
|
|
date: '2026-03-19T09:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 18일 (D+18) ──
|
|
{
|
|
text: 'CENTCOM: Houthi forces launched coordinated mini-submarine torpedo attack against USS Nimitz CSG in Red Sea. All 3 vessels intercepted and destroyed',
|
|
date: '2026-03-18T20:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'CENTCOM: F-22 Raptors conducted first-ever combat operations over Iranian airspace, escorting B-2s striking hardened underground sites near Qom',
|
|
date: '2026-03-18T07:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 17일 (D+17) ──
|
|
{
|
|
text: 'CENTCOM: B-2 stealth bombers and GBU-57 MOPs successfully struck the Fordow Fuel Enrichment Plant. Underground enrichment halls confirmed destroyed',
|
|
date: '2026-03-17T10:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'BREAKING: Iran\'s new Supreme Leader Mojtaba Khamenei issues statement delegating "pre-authorized nuclear retaliation" to IRGC. UN Security Council convenes emergency session',
|
|
date: '2026-03-17T05:30:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 16일 (D+16) ──
|
|
{
|
|
text: 'CENTCOM: Isfahan military complex struck overnight by B-2 stealth bombers. 15 targets destroyed including underground command bunkers',
|
|
date: '2026-03-16T06:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'UPDATE: Iran FM Araghchi states "We never asked for a ceasefire." CENTCOM forces maintain full operational tempo across theater',
|
|
date: '2026-03-16T03:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 15일 (D+15) ──
|
|
{
|
|
text: 'CENTCOM: Over 5,000 targets struck since Operation Epic Fury began. Iran offensive missile capability assessed below 3%',
|
|
date: '2026-03-15T18:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'NATO air defenses intercept THIRD Iranian ballistic missile over Turkish airspace near Hatay Province. No casualties reported',
|
|
date: '2026-03-15T12:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'CENTCOM confirms 8 US service members KIA, ~140 WIA since start of operations. KC-135 crash in Iraq accounts for 6 of the fatalities',
|
|
date: '2026-03-15T08:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 14일 (D+14) ──
|
|
{
|
|
text: 'IEA announces largest-ever emergency oil stockpile release: 400 million barrels. Brent crude holds above $103/barrel despite release',
|
|
date: '2026-03-14T20:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'CENTCOM: Trump calls on Korea, Japan, China, France, UK to send warships to Strait of Hormuz to protect commercial shipping',
|
|
date: '2026-03-14T14:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'Pentagon vows to ramp up military campaign against Iran. New supreme leader Mojtaba Khamenei warns retaliatory attacks will continue',
|
|
date: '2026-03-14T06:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 13일 (D+13) ──
|
|
{
|
|
text: 'CENTCOM: Hormuz Strait commercial shipping corridor now 95% mine-free. 28 mines cleared in past 72hrs by US, UK, French minesweeping forces',
|
|
date: '2026-03-13T08:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'CENTCOM forces intercept Iranian Shahed-136 drone swarm targeting Al Udeid Air Base, Qatar. All 8 drones destroyed by Patriot & C-RAM systems',
|
|
date: '2026-03-13T05:30:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'UPDATE: IRGC naval forces have effectively ceased offensive operations in the Strait of Hormuz. Remaining fast boats sheltering in Bandar Abbas harbor',
|
|
date: '2026-03-13T03:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'CENTCOM confirms successful strike on last known IRGC mobile missile TEL in western Iran near Tabriz. Iran\'s offensive missile capability assessed at less than 5%',
|
|
date: '2026-03-13T00:30:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
// ── 3월 12일 (D+12) ──
|
|
{
|
|
text: 'U.S., UK, and French naval forces establish Joint Maritime Security Corridor through Strait of Hormuz. First commercial convoy of 12 tankers transits safely',
|
|
date: '2026-03-12T18:00:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
{
|
|
text: 'USS Michael Murphy (DDG 112) engaged and sank five IRGC fast attack craft in the Strait of Hormuz after they attempted to harass a commercial tanker convoy',
|
|
date: '2026-03-12T04:30:00Z',
|
|
url: 'https://x.com/CENTCOM',
|
|
},
|
|
];
|
|
|
|
async function fetchXCentcom(): Promise<OsintItem[]> {
|
|
const results: OsintItem[] = [];
|
|
|
|
// 1) Nitter/RSSHub 시도 (동작할 경우)
|
|
const rssUrls = [
|
|
`/api/nitter1/CENTCOM/rss`,
|
|
`/api/nitter2/CENTCOM/rss`,
|
|
`/api/nitter3/CENTCOM/rss`,
|
|
`/api/rsshub/twitter/user/CENTCOM`,
|
|
];
|
|
|
|
for (const url of rssUrls) {
|
|
try {
|
|
const res = await fetch(url, { signal: AbortSignal.timeout(6000), redirect: 'error' });
|
|
if (!res.ok) continue;
|
|
const text = await res.text();
|
|
const parser = new DOMParser();
|
|
const xml = parser.parseFromString(text, 'text/xml');
|
|
let items = xml.querySelectorAll('item');
|
|
if (items.length === 0) items = xml.querySelectorAll('entry');
|
|
if (items.length === 0) continue;
|
|
|
|
items.forEach((item, i) => {
|
|
const rawTitle = item.querySelector('title')?.textContent || '';
|
|
const link = item.querySelector('link')?.textContent
|
|
|| item.querySelector('link')?.getAttribute('href') || '';
|
|
const pubDate = item.querySelector('pubDate')?.textContent
|
|
|| item.querySelector('published')?.textContent || '';
|
|
const desc = item.querySelector('description')?.textContent
|
|
|| item.querySelector('content')?.textContent || '';
|
|
|
|
const cleanTitle = rawTitle.replace(/<[^>]+>/g, '').trim();
|
|
const cleanDesc = desc.replace(/<[^>]+>/g, '').trim();
|
|
const title = cleanTitle || cleanDesc.slice(0, 280);
|
|
if (!title || title.length < 5) return;
|
|
|
|
const loc = extractLocation(title + ' ' + cleanDesc)
|
|
|| extractMELocation(title + ' ' + cleanDesc)
|
|
|| { lat: 26.0, lng: 54.0 };
|
|
const xUrl = link.replace(/nitter\.[^/]+/, 'x.com').replace(/xcancel\.com/, 'x.com')
|
|
|| 'https://x.com/CENTCOM';
|
|
|
|
results.push({
|
|
id: `x-centcom-${i}-${Date.now()}`,
|
|
timestamp: pubDate ? new Date(pubDate).getTime() : Date.now(),
|
|
title: `[CENTCOM] ${title}`,
|
|
source: 'X @CENTCOM',
|
|
url: xUrl,
|
|
category: classifyArticle(title + ' ' + cleanDesc) === 'general' ? 'military' : classifyArticle(title + ' ' + cleanDesc),
|
|
language: 'en',
|
|
lat: loc.lat,
|
|
lng: loc.lng,
|
|
});
|
|
});
|
|
|
|
if (results.length > 0) {
|
|
console.log(`[OSINT] X.com @CENTCOM: ${results.length} posts from RSS`);
|
|
return results;
|
|
}
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// 2) RSS 실패 시 → 수동 관리 CENTCOM 게시물 사용
|
|
console.log('[OSINT] X.com @CENTCOM: RSS unavailable, using curated posts');
|
|
return CENTCOM_POSTS.map((post, i) => {
|
|
const loc = extractMELocation(post.text) || { lat: 26.0, lng: 54.0 };
|
|
return {
|
|
id: `x-centcom-curated-${i}`,
|
|
timestamp: new Date(post.date).getTime(),
|
|
title: `[CENTCOM] ${post.text}`,
|
|
source: 'X @CENTCOM',
|
|
url: post.url,
|
|
category: classifyArticle(post.text) === 'general' ? 'military' : classifyArticle(post.text),
|
|
language: 'en' as const,
|
|
lat: loc.lat,
|
|
lng: loc.lng,
|
|
};
|
|
});
|
|
}
|
|
|
|
// ── Pinned OSINT articles (manually curated) ──
|
|
const PINNED_IRAN: OsintItem[] = [
|
|
// ── 3월 21일 최신 ──
|
|
{
|
|
id: 'pinned-kr-ceasefire-talks-0321',
|
|
timestamp: new Date('2026-03-21T10:00:00+09:00').getTime(),
|
|
title: '[속보] 미-이란, 오만 무스카트서 휴전 협상 2일차… "핵 시설 감시단 수용" 이란 내부 검토',
|
|
source: '연합뉴스',
|
|
url: 'https://www.yna.co.kr',
|
|
category: 'diplomacy',
|
|
language: 'ko',
|
|
lat: 23.58, lng: 58.40,
|
|
},
|
|
{
|
|
id: 'pinned-kr-oil-drop-0321',
|
|
timestamp: new Date('2026-03-21T08:00:00+09:00').getTime(),
|
|
title: '브렌트유 $97로 하락… 휴전 협상 기대감 반영, 한국 정유사 비축유 방출 중단 검토',
|
|
source: '매일경제',
|
|
url: 'https://www.mk.co.kr',
|
|
category: 'oil',
|
|
language: 'ko',
|
|
lat: 37.57, lng: 126.98,
|
|
},
|
|
{
|
|
id: 'pinned-kr-hormuz-72pct-0321',
|
|
timestamp: new Date('2026-03-21T06:00:00+09:00').getTime(),
|
|
title: '호르무즈 해협 통항량 72% 회복… 한국 수입 유조선 5척 오늘 무사 통과',
|
|
source: 'SBS',
|
|
url: 'https://news.sbs.co.kr',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
// ── 3월 20일 ──
|
|
{
|
|
id: 'pinned-kr-muscat-talks-0320',
|
|
timestamp: new Date('2026-03-20T20:00:00+09:00').getTime(),
|
|
title: '[긴급] 미-이란 협상단 오만 무스카트 회동 확인… 오만 외무 중재, 핵 동결 조건 논의',
|
|
source: 'KBS',
|
|
url: 'https://news.kbs.co.kr',
|
|
category: 'diplomacy',
|
|
language: 'ko',
|
|
lat: 23.58, lng: 58.40,
|
|
},
|
|
{
|
|
id: 'pinned-kr-irgc-flee-0320',
|
|
timestamp: new Date('2026-03-20T14:00:00+09:00').getTime(),
|
|
title: 'IRGC 고위 사령관 다수, 러시아 망명 정황 포착… 이란 지휘체계 붕괴 우려',
|
|
source: '조선일보',
|
|
url: 'https://www.chosun.com',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 35.69, lng: 51.39,
|
|
},
|
|
{
|
|
id: 'pinned-kr-tanker-return-0320',
|
|
timestamp: new Date('2026-03-20T09:00:00+09:00').getTime(),
|
|
title: '한국 유조선 "광양 파이오니어호" 호르무즈 통과 성공… 30일 만에 첫 정상 귀항',
|
|
source: '해사신문',
|
|
url: 'https://www.haesanews.com',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
// ── 3월 19일 ──
|
|
{
|
|
id: 'pinned-kr-iran-ceasefire-0319',
|
|
timestamp: new Date('2026-03-19T18:00:00+09:00').getTime(),
|
|
title: '[속보] 이란, 오만 채널 통해 "무조건 휴전 협상 준비" 신호… 미국 "확인 중"',
|
|
source: '연합뉴스',
|
|
url: 'https://www.yna.co.kr',
|
|
category: 'diplomacy',
|
|
language: 'ko',
|
|
lat: 35.69, lng: 51.39,
|
|
},
|
|
{
|
|
id: 'pinned-kr-ko-reserves-0319',
|
|
timestamp: new Date('2026-03-19T12:00:00+09:00').getTime(),
|
|
title: '정부 "원유 수급 숨통 트였다"… 비축유 80일분 유지·추가 방출 잠정 보류',
|
|
source: '서울경제',
|
|
url: 'https://en.sedaily.com',
|
|
category: 'oil',
|
|
language: 'ko',
|
|
lat: 37.57, lng: 126.98,
|
|
},
|
|
// ── 3월 18일 ──
|
|
{
|
|
id: 'pinned-kr-houthi-sub-0318',
|
|
timestamp: new Date('2026-03-18T22:00:00+09:00').getTime(),
|
|
title: '예멘 후티, 미 항공모함 겨냥 소형 잠수정 어뢰 공격 시도… 미 해군 3척 격침',
|
|
source: 'BBC Korea',
|
|
url: 'https://www.bbc.com/korean',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 14.80, lng: 42.95,
|
|
},
|
|
{
|
|
id: 'pinned-kr-f22-0318',
|
|
timestamp: new Date('2026-03-18T08:00:00+09:00').getTime(),
|
|
title: 'F-22 랩터, 이란 상공 첫 실전 투입 확인… B-2 호위하며 쿰 인근 지하시설 공격',
|
|
source: '조선일보',
|
|
url: 'https://www.chosun.com',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 34.64, lng: 50.88,
|
|
},
|
|
// ── 3월 17일 ──
|
|
{
|
|
id: 'pinned-kr-fordow-0317',
|
|
timestamp: new Date('2026-03-17T12:00:00+09:00').getTime(),
|
|
title: '[속보] 미군, 포르도 핵연료 농축시설 벙커버스터 공격… 지하 격납고 완파 확인',
|
|
source: '연합뉴스',
|
|
url: 'https://www.yna.co.kr',
|
|
category: 'nuclear',
|
|
language: 'ko',
|
|
lat: 34.88, lng: 49.93,
|
|
},
|
|
{
|
|
id: 'pinned-kr-nuclear-threat-0317',
|
|
timestamp: new Date('2026-03-17T07:00:00+09:00').getTime(),
|
|
title: '이란 최고지도자, IRGC에 "선제 핵 보복 권한 위임" 발표… UN 안보리 긴급 소집',
|
|
source: 'MBC',
|
|
url: 'https://imnews.imbc.com',
|
|
category: 'nuclear',
|
|
language: 'ko',
|
|
lat: 35.69, lng: 51.39,
|
|
},
|
|
// ── 3월 16일 ──
|
|
{
|
|
id: 'pinned-kr-isfahan-0316',
|
|
timestamp: new Date('2026-03-16T10:00:00+09:00').getTime(),
|
|
title: '[속보] 미-이스라엘, 이스파한 군사시설 야간 폭격… 이란 사망자 1,444명 돌파',
|
|
source: '연합뉴스',
|
|
url: 'https://www.yna.co.kr',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 32.65, lng: 51.67,
|
|
},
|
|
{
|
|
id: 'pinned-kr-ceasefire-0316',
|
|
timestamp: new Date('2026-03-16T08:00:00+09:00').getTime(),
|
|
title: '이란 외무 "휴전 요청한 적 없다"… 트럼프 "이란이 딜 원해" 주장 정면 반박',
|
|
source: 'VOA Korea',
|
|
url: 'https://www.voakorea.com',
|
|
category: 'diplomacy',
|
|
language: 'ko',
|
|
lat: 35.69, lng: 51.39,
|
|
},
|
|
// ── 3월 15일 ──
|
|
{
|
|
id: 'pinned-kr-hormuz-파병-0315',
|
|
timestamp: new Date('2026-03-15T18:00:00+09:00').getTime(),
|
|
title: '[단독] 트럼프, 한국 등 5개국에 호르무즈 군함 파견 요구… 청해부대식 파병 논의',
|
|
source: '뉴데일리',
|
|
url: 'https://www.newdaily.co.kr',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
{
|
|
id: 'pinned-kr-kospi-0315',
|
|
timestamp: new Date('2026-03-15T09:00:00+09:00').getTime(),
|
|
title: '이란 전쟁 장기화에 코스피 4,800선 위협… 시총 500조원 이상 증발',
|
|
source: '뉴데일리',
|
|
url: 'https://biz.newdaily.co.kr',
|
|
category: 'oil',
|
|
language: 'ko',
|
|
lat: 37.57, lng: 126.98,
|
|
},
|
|
// ── 3월 14일 ──
|
|
{
|
|
id: 'pinned-kr-iea-oil-0314',
|
|
timestamp: new Date('2026-03-14T20:00:00+09:00').getTime(),
|
|
title: 'IEA 사상 최대 4억 배럴 비축유 방출 결정… 브렌트유 $103 여전히 고공행진',
|
|
source: '매일경제',
|
|
url: 'https://www.mk.co.kr',
|
|
category: 'oil',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
{
|
|
id: 'pinned-kr-hormuz-shutdown-0314',
|
|
timestamp: new Date('2026-03-14T14:00:00+09:00').getTime(),
|
|
title: '호르무즈 해협 "사실상 마비"… 한국 원유 70% 중동산, 수급 비상등',
|
|
source: 'iFM',
|
|
url: 'https://news.ifm.kr',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
{
|
|
id: 'pinned-kr-tanker-0314',
|
|
timestamp: new Date('2026-03-14T10:00:00+09:00').getTime(),
|
|
title: '한국 해운사 시노코르, 유조선 용선료 10배 폭등 $50만/일… 전쟁 특수',
|
|
source: 'Bloomberg Korea',
|
|
url: 'https://www.bloomberg.com',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
// ── 3월 13일 ──
|
|
{
|
|
id: 'pinned-kr-hormuz-0313a',
|
|
timestamp: new Date('2026-03-13T09:00:00+09:00').getTime(),
|
|
title: '[속보] 호르무즈 해협 안전항행 회랑 설정… 한국행 유조선 3척 통과 성공',
|
|
source: '연합뉴스',
|
|
url: 'https://www.yna.co.kr',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
{
|
|
id: 'pinned-kr-hormuz-0313b',
|
|
timestamp: new Date('2026-03-13T08:00:00+09:00').getTime(),
|
|
title: '[단독] 청해부대 "문무대왕함" 호르무즈 해협 진입… 한국 선박 호위 개시',
|
|
source: 'KBS',
|
|
url: 'https://news.kbs.co.kr',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 26.30, lng: 56.50,
|
|
},
|
|
{
|
|
id: 'pinned-kr-ship-0312',
|
|
timestamp: new Date('2026-03-12T18:00:00+09:00').getTime(),
|
|
title: '[긴급] 한국 LNG선 "SK이노베이션호" 이란 드론 피격… 선체 경미 손상, 승조원 무사',
|
|
source: 'SBS',
|
|
url: 'https://news.sbs.co.kr',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.20, lng: 56.60,
|
|
},
|
|
];
|
|
|
|
// ── Pinned OSINT articles (Korea maritime/security) ──
|
|
const PINNED_KOREA: OsintItem[] = [
|
|
// ── 3월 21일 최신 ──
|
|
{
|
|
id: 'pin-kr-cn-fishing-0321',
|
|
timestamp: new Date('2026-03-21T09:00:00+09:00').getTime(),
|
|
title: '[속보] 중국어선 250척 이상 서해 EEZ 집단 침범… 해경 함정 12척 긴급 출동',
|
|
source: '연합뉴스',
|
|
url: 'https://www.yna.co.kr',
|
|
category: 'fishing',
|
|
language: 'ko',
|
|
lat: 37.20, lng: 124.80,
|
|
},
|
|
{
|
|
id: 'pin-kr-hormuz-talks-0321',
|
|
timestamp: new Date('2026-03-21T07:00:00+09:00').getTime(),
|
|
title: '정부 "이란 협상 타결 시 비축유 방출 중단"… 원유 수급 정상화 기대감',
|
|
source: '서울경제',
|
|
url: 'https://en.sedaily.com',
|
|
category: 'oil',
|
|
language: 'ko',
|
|
lat: 37.57, lng: 126.98,
|
|
},
|
|
// ── 3월 20일 ──
|
|
{
|
|
id: 'pin-kr-jmsdf-0320',
|
|
timestamp: new Date('2026-03-20T16:00:00+09:00').getTime(),
|
|
title: '한미일 공동 해상 순찰 강화… F-35B 탑재 JMSDF 함정 동해 합류',
|
|
source: '국방일보',
|
|
url: 'https://www.kookbang.com',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 37.50, lng: 130.00,
|
|
},
|
|
{
|
|
id: 'pin-kr-mof-38ships-0320',
|
|
timestamp: new Date('2026-03-20T11:00:00+09:00').getTime(),
|
|
title: '해양수산부, 호르무즈 인근 한국 선박 38척 안전 관리 중… 2척 귀항 성공',
|
|
source: '해사신문',
|
|
url: 'https://www.haesanews.com',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
// ── 3월 19일 ──
|
|
{
|
|
id: 'pin-kr-coast-guard-crackdown-0319',
|
|
timestamp: new Date('2026-03-19T10:00:00+09:00').getTime(),
|
|
title: '해경, 서해5도 꽃게 시즌 앞두고 중국 불법어선 특별단속… 18척 나포, 350척 검문',
|
|
source: '아시아경제',
|
|
url: 'https://www.asiae.co.kr',
|
|
category: 'fishing',
|
|
language: 'ko',
|
|
lat: 37.67, lng: 125.70,
|
|
},
|
|
// ── 3월 18일 ──
|
|
{
|
|
id: 'pin-kr-nk-response-0318',
|
|
timestamp: new Date('2026-03-18T14:00:00+09:00').getTime(),
|
|
title: '북한, 이란 전황 관련 "반미 연대" 성명 발표… 군사정보 공유 가능성 주목',
|
|
source: 'KBS',
|
|
url: 'https://news.kbs.co.kr',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 39.00, lng: 125.75,
|
|
},
|
|
// ── 3월 17일 ──
|
|
{
|
|
id: 'pin-kr-coast-guard-seizure-0317',
|
|
timestamp: new Date('2026-03-17T09:00:00+09:00').getTime(),
|
|
title: '[단독] 해경, 올해 최대 규모 중국어선 동시 나포… 부산 해경서 20척 압류·선원 47명 조사',
|
|
source: '연합뉴스',
|
|
url: 'https://www.yna.co.kr',
|
|
category: 'fishing',
|
|
language: 'ko',
|
|
lat: 35.10, lng: 129.04,
|
|
},
|
|
// ── 3월 15일 ──
|
|
{
|
|
id: 'pin-kr-nk-missile-0315',
|
|
timestamp: new Date('2026-03-15T07:00:00+09:00').getTime(),
|
|
title: '[속보] 북한, 동해상으로 탄도미사일 약 10발 발사… 350km 비행',
|
|
source: '연합뉴스',
|
|
url: 'https://www.yna.co.kr',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 39.00, lng: 127.00,
|
|
},
|
|
{
|
|
id: 'pin-kr-nk-kimyojong-0315',
|
|
timestamp: new Date('2026-03-15T10:00:00+09:00').getTime(),
|
|
title: '김여정 "전술핵으로 상대 군사인프라 생존 불가"… 자유의 방패 훈련 반발',
|
|
source: 'KBS',
|
|
url: 'https://news.kbs.co.kr',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 39.00, lng: 125.75,
|
|
},
|
|
{
|
|
id: 'pin-kr-hormuz-deploy-0315',
|
|
timestamp: new Date('2026-03-15T18:00:00+09:00').getTime(),
|
|
title: '트럼프, 한국 등 5개국에 호르무즈 군함 파견 요구… 청해부대 파병 논의 본격화',
|
|
source: '뉴데일리',
|
|
url: 'https://www.newdaily.co.kr',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
// ── 3월 14일 ──
|
|
{
|
|
id: 'pin-kr-freedom-shield-0314',
|
|
timestamp: new Date('2026-03-14T09:00:00+09:00').getTime(),
|
|
title: '한미 자유의 방패 2026 훈련 진행 중… 미군 일부 중동 전환 배치에도 "방위태세 문제 없어"',
|
|
source: 'MBC',
|
|
url: 'https://imnews.imbc.com',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 37.50, lng: 127.00,
|
|
},
|
|
{
|
|
id: 'pin-kr-hmm-0314',
|
|
timestamp: new Date('2026-03-14T15:00:00+09:00').getTime(),
|
|
title: 'HMM 선박 6~7척 호르무즈 인근 대기 중… 해운업계 운임 50~80% 급등',
|
|
source: '해사신문',
|
|
url: 'https://www.haesanews.com',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.00, lng: 56.00,
|
|
},
|
|
// ── 3월 13일 ──
|
|
{
|
|
id: 'pin-kr-fuel-cap-0313',
|
|
timestamp: new Date('2026-03-13T12:00:00+09:00').getTime(),
|
|
title: '[속보] 정부, 1997년 이후 첫 유류 가격 상한제 시행… 휘발유 1,724원/L 상한',
|
|
source: '서울경제',
|
|
url: 'https://en.sedaily.com',
|
|
category: 'oil',
|
|
language: 'ko',
|
|
lat: 37.57, lng: 126.98,
|
|
},
|
|
{
|
|
id: 'pin-kr-coast-guard-0313',
|
|
timestamp: new Date('2026-03-13T08:00:00+09:00').getTime(),
|
|
title: '해경, 서해5도 꽃게 시즌 대비 중국 불법어선 단속 강화… 6척 나포, 241척 검문',
|
|
source: '아시아경제',
|
|
url: 'https://www.asiae.co.kr',
|
|
category: 'maritime_traffic',
|
|
language: 'ko',
|
|
lat: 37.67, lng: 125.70,
|
|
},
|
|
{
|
|
id: 'pin-kr-nk-destroyer-0312',
|
|
timestamp: new Date('2026-03-12T16:00:00+09:00').getTime(),
|
|
title: '북한 최현급 구축함, 순항미사일 시험 발사 확인… VLS 88셀로 증강',
|
|
source: 'AEI/국방일보',
|
|
url: 'https://www.aei.org',
|
|
category: 'military',
|
|
language: 'ko',
|
|
lat: 39.80, lng: 127.50,
|
|
},
|
|
{
|
|
id: 'pin-kr-oil-reserve-0312',
|
|
timestamp: new Date('2026-03-12T14:00:00+09:00').getTime(),
|
|
title: '한국, IEA 공조로 전략비축유 역대 최대 2,246만 배럴 방출… 잔여 7,764만 배럴',
|
|
source: '한국경제',
|
|
url: 'https://www.hankyung.com',
|
|
category: 'oil',
|
|
language: 'ko',
|
|
lat: 36.97, lng: 126.83,
|
|
},
|
|
{
|
|
id: 'pin-kr-chinese-fishing-0311',
|
|
timestamp: new Date('2026-03-11T09:00:00+09:00').getTime(),
|
|
title: '서해 NLL 인근 중국 불법어선 하루 200척 이상… "어획량 1/3로 급감" 어민 호소',
|
|
source: '아시아A',
|
|
url: 'https://www.asiaa.co.kr',
|
|
category: 'fishing',
|
|
language: 'ko',
|
|
lat: 37.67, lng: 125.50,
|
|
},
|
|
{
|
|
id: 'pin-kr-ships-hormuz-0311',
|
|
timestamp: new Date('2026-03-11T07:00:00+09:00').getTime(),
|
|
title: '호르무즈 해협 내 한국 국적선 26척·한국인 144명 체류 확인… 미사일 100m 근접 피격 증언',
|
|
source: '서울신문',
|
|
url: 'https://www.seoul.co.kr',
|
|
category: 'shipping',
|
|
language: 'ko',
|
|
lat: 26.56, lng: 56.25,
|
|
},
|
|
];
|
|
|
|
// ── Main fetch: merge all sources, deduplicate, sort by time ──
|
|
async function fetchOsintFromBackend(region: 'iran' | 'korea'): Promise<OsintItem[]> {
|
|
try {
|
|
const res = await fetch(`/api/kcg/osint?region=${region}`, { credentials: 'include' });
|
|
if (!res.ok) return [];
|
|
const data = await res.json();
|
|
return (data.items ?? []) as OsintItem[];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function fetchOsintFeed(focus: 'iran' | 'korea' = 'iran'): Promise<OsintItem[]> {
|
|
// 백엔드 API 우선 시도
|
|
const backendItems = await fetchOsintFromBackend(focus);
|
|
if (backendItems.length > 0) {
|
|
return backendItems;
|
|
}
|
|
|
|
// 백엔드 실패 시 직접 호출 fallback
|
|
const gdeltKw = focus === 'korea' ? GDELT_KEYWORDS_KOREA : GDELT_KEYWORDS_IRAN;
|
|
const gnKrKw = focus === 'korea' ? GNEWS_KR_KOREA : GNEWS_KR_IRAN;
|
|
const gnEnKw = focus === 'korea' ? GNEWS_EN_KOREA : GNEWS_EN_IRAN;
|
|
|
|
const sources = [
|
|
fetchGDELT(gdeltKw),
|
|
fetchGoogleNewsKR(gnKrKw),
|
|
fetchGoogleNewsEN(gnEnKw),
|
|
...(focus === 'iran' ? [fetchXCentcom()] : []),
|
|
];
|
|
const results = await Promise.allSettled(sources);
|
|
|
|
const pinned = focus === 'iran' ? PINNED_IRAN : PINNED_KOREA;
|
|
const all: OsintItem[] = [
|
|
...pinned,
|
|
...results.flatMap(r => r.status === 'fulfilled' ? r.value : []),
|
|
];
|
|
|
|
// Filter out irrelevant articles for Korea feed
|
|
const KOREA_NOISE = /쿠팡|수산시장|맛집|레시피|요리|축제|관광|여행|부동산|아파트|주식|코스피|연예|드라마|영화|스포츠|야구|축구|골프|coupang|recipe|tourism|real estate/i;
|
|
const filtered = focus === 'korea'
|
|
? all.filter(item => !KOREA_NOISE.test(item.title))
|
|
: all;
|
|
|
|
// Deduplicate by similar title (first 40 chars)
|
|
const seen = new Set<string>();
|
|
const unique = filtered.filter(item => {
|
|
const key = item.title.slice(0, 40).toLowerCase();
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
|
|
// Sort newest first
|
|
unique.sort((a, b) => b.timestamp - a.timestamp);
|
|
|
|
return unique.slice(0, 50); // cap at 50 items
|
|
}
|