<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>주요 뉴스 위젯</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- React 및 Babel CDN -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
body { font-family: 'Pretendard', sans-serif; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 스크롤바 숨기기 */
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useCallback } = React;
// --- 아이콘 컴포넌트 ---
const NewspaperIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"></path>
<path d="M18 14h-8"></path><path d="M15 18h-5"></path><path d="M10 6h8v4h-8V6Z"></path>
</svg>
);
const ExternalLinkIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" x2="21" y1="14" y2="3"></line>
</svg>
);
const RefreshIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path><path d="M8 16H3v5"></path>
</svg>
);
const ClockIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline>
</svg>
);
const ChevronRightIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
);
function NewsWidget() {
const [loading, setLoading] = useState(true);
const [newsData, setNewsData] = useState([]);
const [activeTab, setActiveTab] = useState('latest'); // 'latest' 또는 'popular'
// 뉴스 데이터 가상 fetch 함수
const fetchNews = useCallback(() => {
setLoading(true);
// 실제 환경에서는 API URL에 activeTab 파라미터를 실어 보냅니다.
setTimeout(() => {
const mockData = {
latest: [
{ id: 1, category: '경제', title: '코스피, 외인 매수세에 2600선 회복... 반도체주 강세', source: '경제일보', time: '방금 전', url: 'https://news.naver.com' },
{ id: 2, category: 'IT/과학', title: '차세대 AI 모델 공개 임박, 업계 판도 바뀔까?', source: '테크뉴스', time: '12분 전', url: 'https://news.naver.com' },
{ id: 3, category: '생활', title: '내일부터 전국 흐리고 비... 출근길 우산 챙기세요', source: '웨더뉴스', time: '45분 전', url: 'https://news.naver.com' }
],
popular: [
{ id: 10, category: '사회', title: '주말 고속도로 정체 극심... 평소보다 2시간 더 소요', source: '연합뉴스', time: '1시간 전', url: 'https://news.naver.com' },
{ id: 11, category: '경제', title: '금리 동결 결정에 시장 안도... 환율 안정세', source: '금융신문', time: '2시간 전', url: 'https://news.naver.com' },
{ id: 12, category: 'IT', title: '신형 스마트폰 사전예약 폭주... 재고 부족 우려', source: '디지털포커스', time: '3시간 전', url: 'https://news.naver.com' }
]
};
setNewsData(mockData[activeTab]);
setLoading(false);
}, 600);
}, [activeTab]);
// 탭이 바뀌거나 처음 로드될 때 실행
useEffect(() => {
fetchNews();
}, [fetchNews]);
// 더보기 버튼 클릭 시 네이버 뉴스로 이동
const handleMoreClick = () => {
window.open('https://news.naver.com', '_blank');
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-slate-900 to-black flex items-center justify-center p-4">
<div className="bg-gray-800 rounded-3xl shadow-2xl overflow-hidden max-w-sm w-full relative border border-gray-700 flex flex-col h-[520px]">
{/* 헤더 섹션 */}
<div className="relative z-10 p-6 pb-4 bg-gray-800/90 backdrop-blur-md border-b border-gray-700">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<div className="p-2 bg-rose-500 rounded-lg shadow-lg shadow-rose-500/20">
<NewspaperIcon className="w-5 h-5 text-white" />
</div>
<span className="text-white font-bold text-lg">주요 뉴스</span>
</div>
<button
onClick={fetchNews}
disabled={loading}
title="새로고침"
className="p-2 hover:bg-gray-700 rounded-full transition-colors text-gray-400 hover:text-white"
>
<RefreshIcon className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
{/* 탭 버튼 */}
<div className="flex space-x-1 bg-gray-700/50 p-1 rounded-xl">
<button
onClick={() => setActiveTab('latest')}
className={`flex-1 py-1.5 text-xs font-medium rounded-lg transition-all ${
activeTab === 'latest' ? 'bg-gray-600 text-white shadow-sm' : 'text-gray-400 hover:text-gray-200'
}`}
>
최신순
</button>
<button
onClick={() => setActiveTab('popular')}
className={`flex-1 py-1.5 text-xs font-medium rounded-lg transition-all ${
activeTab === 'popular' ? 'bg-gray-600 text-white shadow-sm' : 'text-gray-400 hover:text-gray-200'
}`}
>
많이 본 뉴스
</button>
</div>
</div>
{/* 리스트 섹션 */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 hide-scrollbar">
{loading ? (
<div className="flex flex-col items-center justify-center h-full space-y-3">
<div className="w-8 h-8 border-2 border-rose-500 border-t-transparent rounded-full animate-spin"></div>
<span className="text-xs text-gray-500 font-medium">데이터를 가져오는 중...</span>
</div>
) : (
newsData.map((news) => (
<a
key={news.id}
href={news.url}
target="_blank"
className="block group bg-gray-700/30 hover:bg-gray-700/60 rounded-xl p-4 transition-all duration-200 border border-gray-700/50 hover:border-gray-600 active:scale-[0.98]"
>
<div className="flex justify-between items-start mb-2">
<span className="px-2 py-0.5 rounded-md bg-gray-600/50 text-gray-300 text-[10px] font-medium border border-gray-600">
{news.category}
</span>
<ExternalLinkIcon className="w-3 h-3 text-gray-500 group-hover:text-rose-400 transition-colors" />
</div>
<h3 className="text-gray-100 font-semibold text-sm leading-snug mb-3 line-clamp-2 group-hover:text-rose-100 transition-colors">
{news.title}
</h3>
<div className="flex items-center justify-between text-[11px] text-gray-500">
<span className="font-medium text-gray-400">{news.source}</span>
<div className="flex items-center space-x-1">
<ClockIcon className="w-3 h-3" />
<span>{news.time}</span>
</div>
</div>
</a>
))
)}
</div>
{/* 푸터 섹션 (더보기 버튼) */}
<div className="p-4 bg-gray-800 border-t border-gray-700">
<button
onClick={handleMoreClick}
className="w-full py-2.5 flex items-center justify-center space-x-1 text-sm font-medium text-gray-400 hover:text-white bg-gray-700/40 hover:bg-gray-700 rounded-xl transition-all group"
>
<span>뉴스 더보기</span>
<ChevronRightIcon className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NewsWidget />);
</script>
</body>
</html>