import React, { useEffect, useRef, useState } from 'react'; import { useAuth } from '../context/AuthContext'; import { API_BASE_URL } from '../config'; function normalizeExternalUrl(raw) { if (!raw) return ''; const s = String(raw).trim(); if (!s) return ''; // already absolute if (/^https?:\/\//i.test(s)) return s; // protocol-relative if (s.startsWith('//')) return `https:${s}`; // common "missing scheme" cases from DB/meta if (s.startsWith('img.youtube.com/')) return `https://${s}`; if (s.startsWith('www.youtube.com/')) return `https://${s}`; if (s.startsWith('youtube.com/')) return `https://${s}`; return s; // leave as-is (could be relative path on same origin) } export default function AiNewsPage() { const { user } = useAuth(); const [news, setNews] = useState([]); const [newsOffset, setNewsOffset] = useState(0); const [newsLoading, setNewsLoading] = useState(false); const [showNewsEditor, setShowNewsEditor] = useState(false); const [newsUrl, setNewsUrl] = useState(''); const contentRef = useRef(null); const sentinelRef = useRef(null); useEffect(() => { (async () => { await loadNews(0, false); // 최초 로드 후 최신 위치로 한 번 스크롤 requestAnimationFrame(() => { const el = contentRef.current; if (el) el.scrollTop = el.scrollHeight; }); })(); }, []); // IntersectionObserver로 상단 도달 시 로드 useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && !newsLoading) { loadNews(newsOffset, true); } }, { root: contentRef.current, threshold: 0.1 } ); if (sentinelRef.current) observer.observe(sentinelRef.current); return () => observer.disconnect(); }, [newsOffset, newsLoading]); const loadNews = async (offset = 0, prepend = false) => { if (newsLoading) return; setNewsLoading(true); try { const res = await fetch(`${API_BASE_URL}/community/ai_news?offset=${offset}&limit=10`); if (!res.ok) return; const data = await res.json(); const items = (data.items || []).reverse(); if (prepend) { const el = contentRef.current; const prevH = el ? el.scrollHeight : document.documentElement.scrollHeight; setNews((prev) => [...items, ...prev]); setTimeout(() => { if (el) { el.scrollTop = el.scrollHeight - prevH; } else { const newH = document.documentElement.scrollHeight; window.scrollTo(0, newH - prevH); } }, 0); } else { setNews(items); setTimeout(() => { const el = contentRef.current; if (el) el.scrollTop = el.scrollHeight; else window.scrollTo(0, document.documentElement.scrollHeight); }, 0); } const nextOffset = data.nextOffset || 0; setNewsOffset(nextOffset); return nextOffset; } finally { setNewsLoading(false); } }; // 기존 스크롤 보정 루프 제거 – IntersectionObserver 사용 const submitNews = async () => { if (!newsUrl.trim()) return; await fetch(`${API_BASE_URL}/community/ai_news`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: newsUrl, author_id: String(user?.user_id || ''), author_email: user?.email || null }), }); setShowNewsEditor(false); setNewsUrl(''); loadNews(0); }; return (

인사이트

업계 동향/레퍼런스 링크를 모아두고, 팀/고객과 빠르게 공유합니다.

{news.map((n) => (
{n.meta?.image ? ( thumb ) : null}
{n.meta?.title || n.url}
{n.meta?.description}
{new Date(n.created_at).toISOString().slice(0, 10)}
))}
{newsLoading && (
뉴스 로딩중
)} {user && !showNewsEditor && ( )} {showNewsEditor && user && (
setNewsUrl(e.target.value)} placeholder="뉴스 URL을 입력하세요" style={{ width: 'calc(100% - 25px)', marginBottom: 8, padding: '10px 12px', borderRadius: 8, border: '1px solid #ffd6c2' }} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); submitNews(); } }} />
)}
); }