initial commit

This commit is contained in:
2026-02-04 00:16:34 +09:00
commit ae11528dd9
867 changed files with 209640 additions and 0 deletions

180
frontend/stocks.html Normal file
View 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>