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

446
frontend/index.html Normal file
View File

@@ -0,0 +1,446 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KisStock AI Dashboard</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav>
<div class="logo">KisStock AI</div>
<div class="nav-links">
<a href="/" class="active">대시보드</a>
<a href="/trade">매매</a>
<a href="/news">AI뉴스</a>
<a href="/stocks">종목</a>
<a href="/settings">설정</a>
</div>
</nav>
<div class="container">
<!-- Balance Section -->
<div class="card">
<h2>💰 나의 자산</h2>
<div id="balance-info" class="loading-container">
<div class="grid">
<div>
<div class="text-muted">총 평가금액</div>
<div class="h2" id="total-eval">Loading...</div>
</div>
<div>
<div class="text-muted">예수금</div>
<div class="h2" id="deposit">Loading...</div>
</div>
<div>
<div class="text-muted">손익</div>
<div class="h2" id="total-profit">Loading...</div>
</div>
</div>
</div>
</div>
<div class="grid">
<!-- Watchlist Section -->
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center;">
<h2>⭐ 관심 종목</h2>
<a href="/stocks" class="btn" style="padding:0.2rem 0.5rem; font-size:0.8rem;">종목 추가</a>
</div>
<div id="watchlist-container" class="grid" style="grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap:1rem; margin-top:1rem;">
Loading...
</div>
</div>
<!-- Holdings Section -->
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center;">
<h2>📊 보유 종목</h2>
<div>
<button class="btn btn-sm" onclick="refreshHoldings()" style="margin-right:8px; padding:4px 8px; font-size:0.8rem; background:#475569;">🔄 조회</button>
<button id="btn-domestic" class="active-tab" onclick="showHoldingsTab('domestic')">국내</button>
<button id="btn-overseas" class="inactive-tab" onclick="showHoldingsTab('overseas')">해외(NASD)</button>
</div>
</div>
<style>
.active-tab { background: var(--accent-color); color:white; border:none; padding:4px 8px; border-radius:4px; font-weight:bold; cursor:pointer; }
.inactive-tab { background: transparent; color:#888; border:1px solid #555; padding:4px 8px; border-radius:4px; cursor:pointer; }
</style>
<div style="overflow-x:auto;">
<table id="holdings-table">
<thead>
<tr>
<th>종목명</th>
<th>잔고</th>
<th>현재가</th>
<th>손익률</th>
</tr>
</thead>
<tbody>
<!-- Rows -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Recent Orders -->
<div class="card">
<h2>📝 최근 주문 내역</h2>
<div style="overflow-x:auto;">
<table id="orders-table">
<thead>
<tr>
<th>시간</th>
<th>종목</th>
<th>구분</th>
<th>가격</th>
<th>수량</th>
<th>상태</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<script>
let currentHoldingsTab = 'domestic';
let lastFilledCount = -1;
function showHoldingsTab(tab) {
currentHoldingsTab = tab;
document.getElementById('btn-domestic').className = tab === 'domestic' ? 'active-tab' : 'inactive-tab';
document.getElementById('btn-overseas').className = tab === 'overseas' ? 'active-tab' : 'inactive-tab';
// Clear table and fetch immediately
document.querySelector('#holdings-table tbody').innerHTML = '<tr><td colspan="4">Loading...</td></tr>';
refreshHoldings();
}
async function refreshHoldings() {
const btn = document.querySelector('button[onclick="refreshHoldings()"]');
const originalText = btn.innerText;
btn.innerText = "⏳ 갱신 중...";
btn.disabled = true;
try {
// Trigger Backend Refresh
await fetch('/api/balance/refresh', { method: 'POST' });
// Then Load Data
if(currentHoldingsTab === 'domestic') {
await fetchBalance('manual_btn');
} else {
await fetchOverseasBalance();
}
} catch(e) {
console.error(e);
alert("갱신 실패");
} finally {
btn.innerText = originalText;
btn.disabled = false;
}
}
async function fetchWatchlist() {
try {
const res = await fetch('/api/watchlist');
const data = await res.json();
const container = document.getElementById('watchlist-container');
container.innerHTML = '';
// Switch to List/Table View
container.style.display = "block"; // Remove grid
container.style.width = "100%";
if (data.length === 0) {
container.innerHTML = "<div class='text-muted' style='text-align:center; padding:2rem;'>등록된 관심 종목이 없습니다.</div>";
return;
}
// Create Table Structure
const table = document.createElement('table');
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.innerHTML = `
<thead style="border-bottom: 1px solid rgba(255,255,255,0.1); color:#94a3b8; font-size:0.9rem;">
<tr>
<th style="padding:1rem; text-align:left;">종목</th>
<th style="padding:1rem; text-align:right;">현재가</th>
<th style="padding:1rem; text-align:right;">등락률</th>
<th style="padding:1rem; text-align:center;">빠른 매매</th>
<th style="padding:1rem; text-align:center;">관리</th>
</tr>
</thead>
<tbody></tbody>
`;
const tbody = table.querySelector('tbody');
data.forEach(item => {
const tr = document.createElement('tr');
tr.id = `watch-${item.code}`;
tr.style.borderBottom = "1px solid rgba(255,255,255,0.05)";
const isOverseas = ['NASD', 'NYSE', 'AMEX'].includes(item.market);
const marketBadge = `<span class="badge ${isOverseas?'text-danger':'text-success'}">${item.market}</span>`;
tr.innerHTML = `
<td style="padding:1rem;">
<div style="font-weight:bold; font-size:1.1rem;">${item.name}</div>
<div style="font-size:0.8rem; color:#64748b;">${item.code} ${marketBadge}</div>
</td>
<td style="padding:1rem; text-align:right;">
<span class="price-tag" style="font-size:1.2rem; font-weight:bold;">-</span>
</td>
<td style="padding:1rem; text-align:right;">
<span class="rate-tag" style="font-size:1rem;">0.00%</span>
</td>
<td style="padding:1rem; text-align:center;">
<div style="display:flex; gap:0.5rem; justify-content:center; align-items:center;">
<button class="btn btn-primary" style="padding:0.4rem 0.8rem;" onclick="goToTrade('${item.code}', '${item.market}', 'buy')">매수</button>
<button class="btn btn-danger" style="padding:0.4rem 0.8rem;" onclick="goToTrade('${item.code}', '${item.market}', 'sell')">매도</button>
</div>
</td>
<td style="padding:1rem; text-align:center;">
<button class="btn btn-danger" onclick="removeWatchlist('${item.code}')" style="padding:0.4rem; font-size:0.8rem;">삭제</button>
</td>
`;
tbody.appendChild(tr);
// Subscribe via WS (If Monitored)
if(item.is_monitoring && ws && ws.readyState === WebSocket.OPEN) {
ws.send(`sub:${item.code}`);
}
});
container.appendChild(table);
} catch(e) { console.error(e); }
}
function goToTrade(code, market, type) {
window.location.href = `/trade.html?code=${code}&market=${market}&type=${type}`;
}
async function removeWatchlist(code) {
if(!confirm("삭제하시겠습니까?")) return;
await fetch(`/api/watchlist/${code}`, { method: 'DELETE' });
fetchWatchlist();
}
async function fetchOverseasBalance() {
try {
const res = await fetch('/api/balance/overseas');
if(!res.ok) throw new Error("Check Server Logs");
const data = await res.json();
const tbody = document.querySelector('#holdings-table tbody');
tbody.innerHTML = '';
if (data.output1 && data.output1.length > 0) {
data.output1.forEach(item => {
const tr = document.createElement('tr');
const name = item.ovrs_item_name || item.prdt_name || item.ovrs_pdno;
const qty = item.ovrs_cblc_qty || item.ord_psbl_qty || item.hldg_qty;
const price = item.now_pric2 || item.prpr || 0;
const profitRate = parseFloat(item.evlu_pfls_rt || 0);
tr.innerHTML = `
<td>${name}</td>
<td>${qty}</td>
<td>$${parseFloat(price).toLocaleString()}</td>
<td class="${profitRate >= 0 ? 'text-success' : 'text-danger'}">${profitRate}%</td>
`;
tbody.appendChild(tr);
});
} else {
tbody.innerHTML = '<tr><td colspan="4">해외 보유 종목이 없습니다.</td></tr>';
}
} catch(e) {
console.error(e);
document.querySelector('#holdings-table tbody').innerHTML = `<tr><td colspan="4">조회 실패/보유없음</td></tr>`;
}
}
async function fetchBalance(source = 'unknown') {
console.log(`Fetching balance via ${source}`);
try {
const res = await fetch(`/api/balance?source=${source}`);
if(!res.ok) throw new Error("Server Error");
const data = await res.json();
if (data.rt_cd && data.rt_cd !== "0") {
throw new Error(data.msg1 || "Unknown API Error");
}
// Keep showing asset summary even if domestic tab is not active
if (data.output2 && data.output2[0]) {
const output2 = data.output2[0];
document.getElementById('total-eval').innerText = parseInt(output2.tot_evlu_amt).toLocaleString() + "원";
document.getElementById('deposit').innerText = parseInt(output2.dnca_tot_amt).toLocaleString() + "원";
const profit = parseInt(output2.evlu_pfls_smtl_amt);
const elProfit = document.getElementById('total-profit');
elProfit.innerText = profit.toLocaleString() + "원";
elProfit.className = profit >= 0 ? "h2 text-success" : "h2 text-danger";
}
if (currentHoldingsTab === 'domestic') {
const tbody = document.querySelector('#holdings-table tbody');
tbody.innerHTML = '';
if (data.output1 && data.output1.length > 0) {
data.output1.forEach(item => {
const tr = document.createElement('tr');
const profitRate = parseFloat(item.evlu_pfls_rt || 0);
tr.innerHTML = `
<td>${item.prdt_name} (${item.pdno})</td>
<td>${item.hldg_qty}</td>
<td>${parseInt(item.prpr).toLocaleString()}</td>
<td class="${profitRate >= 0 ? 'text-success' : 'text-danger'}">${profitRate}%</td>
`;
tbody.appendChild(tr);
});
} else {
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">보유 종목이 없습니다.</td></tr>';
}
}
const now = new Date();
// We might not have this element in previous versions, but let's try safely if it exists, or ignore
// document.getElementById('sync-info').innerText = ...
// Removed to avoid error if element missing
} catch(e) {
console.error(e);
}
}
async function fetchOrders() {
try {
const res = await fetch('/api/orders');
const data = await res.json();
const tbody = document.querySelector('#orders-table tbody');
tbody.innerHTML = '';
let currentFilledCount = 0;
data.forEach(item => {
const tr = document.createElement('tr');
// Simple check for filled status
if (item.status === 'FILLED' || (item.status && item.status.includes('체결'))) {
currentFilledCount++;
}
tr.innerHTML = `
<td>${new Date(item.created_at).toLocaleString()}</td>
<td>${item.code}</td>
<td class="${item.type === 'BUY' ? 'text-danger' : 'text-success'}">${item.type}</td>
<td>${item.price.toLocaleString()}</td>
<td>${item.quantity}</td>
<td>${item.status}</td>
`;
tbody.appendChild(tr);
});
// If number of filled orders increased, refresh balance
// Skip the first check to avoid redundant refresh on page load
if (lastFilledCount === -1) {
lastFilledCount = currentFilledCount;
} else if (currentFilledCount > lastFilledCount) {
console.log("New order filled! Refreshing holdings...");
console.log(`Order count increased: ${lastFilledCount} -> ${currentFilledCount}`);
refreshHoldings();
lastFilledCount = currentFilledCount;
}
} catch(e) { console.error(e); }
}
// Init
fetchBalance('init');
fetchWatchlist();
fetchOrders();
// WebSocket Client
let ws = null;
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log("Connected to Server WS");
// Resend subscriptions for currently displayed items
const items = document.querySelectorAll('[id^="watch-"]');
items.forEach(el => {
const code = el.id.replace('watch-', '');
ws.send(`sub:${code}`);
});
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'PRICE') {
updatePriceDOM(msg);
}
};
ws.onclose = () => {
console.log("WS Disconnected. Reconnecting in 3s...");
setTimeout(connectWebSocket, 3000);
};
}
function updatePriceDOM(data) {
// data: {code, price, change, rate, ...}
// Find elements with data-code attribute? Or just iterate watchlist items?
// Currently our watchlist doesn't have IDs easily addressable.
// We generated: <div style="...">...Code...</div>
// Let's refine fetchWatchlist to add IDs or classes to help update.
// Update Watchlist Items
const watchItem = document.getElementById(`watch-${data.code}`);
if(watchItem) {
const diffColor = parseFloat(data.change) > 0 ? 'text-success' : (parseFloat(data.change) < 0 ? 'text-danger' : 'text-muted');
// Update price text. We need a specific span for price.
// Assuming we rebuild DOM to support this.
// For now, simpler implementation:
// Just flash the card or something?
// No, User wants real update.
const priceEl = watchItem.querySelector('.price-tag');
if(priceEl) {
priceEl.innerText = parseInt(data.price).toLocaleString();
priceEl.className = `price-tag ${diffColor}`;
}
const rateEl = watchItem.querySelector('.rate-tag');
if(rateEl) {
rateEl.innerText = `${data.rate}%`;
rateEl.className = `rate-tag ${diffColor}`;
}
}
}
// Enhance fetchWatchlist to support updates and subscribe
const originalFetchWatchlist = fetchWatchlist;
fetchWatchlist = async function() {
await originalFetchWatchlist(); // Call original
// After rendering, subscribe!
// Wait, original renders HTML. I need to modify it to include IDs.
};
connectWebSocket();
// Refresh Intervals
setInterval(fetchOrders, 30000); // 30s for orders
// setInterval(fetchWatchlist, 60000); // Disable polling if we use WS? Or keep as backup.
</script>
</body>
</html>