This commit is contained in:
dsyoon
2025-12-27 14:57:58 +09:00
parent be8e7c9fd6
commit 7b7e82873e
2 changed files with 68 additions and 7 deletions

View File

@@ -22,6 +22,8 @@ export default function AiNewsPage() {
const [news, setNews] = useState([]);
const [newsOffset, setNewsOffset] = useState(0);
const [newsLoading, setNewsLoading] = useState(false);
const [newsHasMore, setNewsHasMore] = useState(true);
const [newsError, setNewsError] = useState('');
const [showNewsEditor, setShowNewsEditor] = useState(false);
const [newsUrl, setNewsUrl] = useState('');
const contentRef = useRef(null);
@@ -29,6 +31,8 @@ export default function AiNewsPage() {
useEffect(() => {
(async () => {
setNewsError('');
setNewsHasMore(true);
await loadNews(0, false);
// 최초 로드 후 최신 위치로 한 번 스크롤
requestAnimationFrame(() => {
@@ -40,9 +44,10 @@ export default function AiNewsPage() {
// IntersectionObserver로 상단 도달 시 로드
useEffect(() => {
if (!newsHasMore || newsError) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !newsLoading) {
if (entries[0].isIntersecting && !newsLoading && newsHasMore && !newsError) {
loadNews(newsOffset, true);
}
},
@@ -50,16 +55,28 @@ export default function AiNewsPage() {
);
if (sentinelRef.current) observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, [newsOffset, newsLoading]);
}, [newsOffset, newsLoading, newsHasMore, newsError]);
const loadNews = async (offset = 0, prepend = false) => {
if (newsLoading) return;
if (newsLoading || !newsHasMore || newsError) return;
setNewsLoading(true);
try {
const res = await fetch(`${API_BASE_URL}/community/ai_news?offset=${offset}&limit=10`);
if (!res.ok) return;
if (!res.ok) {
// 404는 보통 Apache 프록시 미설정(정적서버가 /community 경로를 못 찾음)
setNewsError(`인사이트 데이터를 불러오지 못했습니다. (HTTP ${res.status})`);
setNewsHasMore(false);
return null;
}
const data = await res.json();
const items = (data.items || []).reverse();
const rawItems = Array.isArray(data.items) ? data.items : [];
// 서버는 보통 최신이 먼저 오므로, UI를 "아래가 최신" 형태로 유지하려고 reverse 유지
const items = rawItems.slice().reverse();
// 더 이상 가져올 게 없으면 observer가 무한 호출하지 않도록 차단
if (rawItems.length === 0) {
setNewsHasMore(false);
}
if (prepend) {
const el = contentRef.current;
const prevH = el ? el.scrollHeight : document.documentElement.scrollHeight;
@@ -79,9 +96,17 @@ export default function AiNewsPage() {
if (el) el.scrollTop = el.scrollHeight; else window.scrollTo(0, document.documentElement.scrollHeight);
}, 0);
}
const nextOffset = data.nextOffset || 0;
setNewsOffset(nextOffset);
const nextOffset = typeof data.nextOffset === 'number' ? data.nextOffset : null;
if (nextOffset === null || nextOffset === offset) {
setNewsHasMore(false);
} else {
setNewsOffset(nextOffset);
}
return nextOffset;
} catch (e) {
setNewsError('인사이트 데이터를 불러오지 못했습니다. (네트워크 오류)');
setNewsHasMore(false);
return null;
} finally {
setNewsLoading(false);
}
@@ -112,6 +137,23 @@ export default function AiNewsPage() {
<main className="community-content" ref={contentRef} style={{ overflowY: 'auto', flex: 1 }}>
<div ref={sentinelRef} />
<div style={{ padding: '0 20px 50px 20px' }}>
{newsError ? (
<div className="card" style={{ marginBottom: 12, borderColor: '#ffd6c2' }}>
<div style={{ fontWeight: 700, marginBottom: 6 }}>인사이트 로딩 실패</div>
<div style={{ color: '#666', marginBottom: 10 }}>{newsError}</div>
<button
className="btn btn-primary"
onClick={() => {
setNewsError('');
setNewsHasMore(true);
setNewsOffset(0);
loadNews(0, false);
}}
>
다시 시도
</button>
</div>
) : null}
{news.map((n) => (
<div key={n.id} className="card" style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', gap: 12 }}>