initial commit
This commit is contained in:
180
frontend/stocks.html
Normal file
180
frontend/stocks.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>종목 정보 - KisStock AI</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div class="logo">KisStock AI</div>
|
||||
<div class="nav-links">
|
||||
<a href="/">대시보드</a>
|
||||
<a href="/trade">매매</a>
|
||||
<a href="/news">AI뉴스</a>
|
||||
<a href="/stocks" class="active">종목</a>
|
||||
<a href="/settings">설정</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||
<h2>📦 전체 종목 DB</h2>
|
||||
<div>
|
||||
<button class="btn" onclick="syncMaster()" style="background:#475569; font-size:0.9rem;">🔄 DB 동기화 (최신 데이터 수집)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid" style="grid-template-columns: 1fr 2fr 1fr; gap:1rem; margin-bottom:1rem;">
|
||||
<select id="market-filter" onchange="searchStocks()">
|
||||
<option value="">전체 시장</option>
|
||||
<option value="KOSPI">KOSPI</option>
|
||||
<option value="KOSDAQ">KOSDAQ</option>
|
||||
<option value="NASD">NASDAQ</option>
|
||||
<option value="NYSE">NYSE</option>
|
||||
<option value="AMEX">AMEX</option>
|
||||
</select>
|
||||
<input type="text" id="search-keyword" placeholder="종목명 또는 코드 검색 (Enter)" onkeypress="if(event.key==='Enter') searchStocks()">
|
||||
<button class="btn btn-primary" onclick="searchStocks()">검색</button>
|
||||
</div>
|
||||
|
||||
<div style="overflow-x:auto;">
|
||||
<table id="stocks-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시장</th>
|
||||
<th>코드</th>
|
||||
<th>종목명</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="pagination" style="margin-top:1rem; text-align:center;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
|
||||
async function searchStocks(page=1) {
|
||||
currentPage = page;
|
||||
const keyword = document.getElementById('search-keyword').value;
|
||||
const market = document.getElementById('market-filter').value;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/stocks?keyword=${encodeURIComponent(keyword)}&market=${market}&page=${page}`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.querySelector('#stocks-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.items.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td><span class="badge ${item.type === 'DOMESTIC' ? 'text-success' : 'text-danger'}">${item.market}</span></td>
|
||||
<td>${item.code}</td>
|
||||
<td>${item.name}</td>
|
||||
<td>
|
||||
<button class="btn" style="padding:0.2rem 0.5rem; font-size:0.8rem;" onclick="addToWatchlist('${item.code}', '${item.name}', '${item.market}')">⭐ 관심등록</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Pagination Logic (Simple)
|
||||
// Assuming API returns total or we just simpler Next/Prev for now
|
||||
document.getElementById('pagination').innerHTML = `
|
||||
<button class="btn" style="width:auto; display:inline-block;" onclick="searchStocks(${page-1})" ${page<=1?'disabled':''}>이전</button>
|
||||
<span style="margin:0 1rem;">Page ${page}</span>
|
||||
<button class="btn" style="width:auto; display:inline-block;" onclick="searchStocks(${page+1})">다음</button>
|
||||
`;
|
||||
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function addToWatchlist(code, name, market) {
|
||||
try {
|
||||
const res = await fetch('/api/watchlist', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ code, name, market })
|
||||
});
|
||||
if(res.ok) alert("관심종목에 추가되었습니다.");
|
||||
else alert("이미 존재하거나 오류가 발생했습니다.");
|
||||
} catch(e) { alert("Error: " + e.message); }
|
||||
}
|
||||
|
||||
async function syncMaster() {
|
||||
if(!confirm("전체 종목 데이터를 서버로 다운로드하고 동기화합니다.\n시간이 소요될 수 있습니다. 진행하시겠습니까?")) return;
|
||||
try {
|
||||
const res = await fetch('/api/sync/master', { method: 'POST' });
|
||||
if(res.ok) {
|
||||
alert("동기화 작업이 백그라운드에서 시작되었습니다. 상태를 모니터링합니다.");
|
||||
pollSyncStatus();
|
||||
}
|
||||
} catch(e) { alert("요청 실패"); }
|
||||
}
|
||||
|
||||
async function pollSyncStatus() {
|
||||
const statusDiv = document.getElementById('sync-status-display') || createStatusDiv();
|
||||
const btn = document.querySelector('button[onclick="syncMaster()"]');
|
||||
|
||||
// Poll every 2s
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/sync/status');
|
||||
const data = await res.json();
|
||||
|
||||
statusDiv.innerHTML = `STATUS: ${data.status.toUpperCase()} - ${data.message}`;
|
||||
|
||||
if (data.status === 'running') {
|
||||
btn.disabled = true;
|
||||
btn.innerText = "⏳ 동기화 중...";
|
||||
statusDiv.className = "alert alert-info";
|
||||
} else if (data.status === 'error') {
|
||||
btn.disabled = false;
|
||||
btn.innerText = "🔄 DB 동기화 (재시도)";
|
||||
statusDiv.className = "alert alert-danger";
|
||||
clearInterval(interval);
|
||||
alert("동기화 중 오류가 발생했습니다: " + data.message);
|
||||
} else if (data.status === 'done') {
|
||||
btn.disabled = false;
|
||||
btn.innerText = "🔄 DB 동기화 (완료)";
|
||||
statusDiv.className = "alert alert-success";
|
||||
clearInterval(interval);
|
||||
alert("동기화 완료!");
|
||||
searchStocks(); // Refresh list
|
||||
} else if (data.status === 'warning') {
|
||||
btn.disabled = false;
|
||||
btn.innerText = "🔄 DB 동기화 (부분완료)";
|
||||
statusDiv.className = "alert alert-warning";
|
||||
clearInterval(interval);
|
||||
alert("부분 완료 (일부 실패): " + data.message);
|
||||
}
|
||||
} catch(e) { clearInterval(interval); }
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function createStatusDiv() {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'sync-status-display';
|
||||
div.style.padding = "10px";
|
||||
div.style.marginBottom = "10px";
|
||||
div.style.borderRadius = "4px";
|
||||
div.style.fontSize = "0.9rem";
|
||||
|
||||
// Insert before grid
|
||||
const container = document.querySelector('.card');
|
||||
container.insertBefore(div, container.children[1]);
|
||||
return div;
|
||||
}
|
||||
|
||||
// Init load
|
||||
searchStocks();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user