first commit

This commit is contained in:
hayato5246
2026-03-31 19:32:59 +09:00
commit d10b794c9f
78 changed files with 1671595 additions and 0 deletions

308
static/js/asset.js Normal file
View File

@@ -0,0 +1,308 @@
/**
* 자산 현황 페이지 로직
* - 요약 카드, 예수금, 보유 종목, 미체결/체결내역
*/
// -----------------------------------
// 초기화
// -----------------------------------
document.addEventListener('DOMContentLoaded', () => {
loadSummary();
loadCash();
loadPending();
});
// -----------------------------------
// 요약 카드 + 보유 종목 (GET /api/account/balance)
// -----------------------------------
async function loadSummary() {
try {
const res = await fetch('/api/account/balance');
const data = await res.json();
if (!res.ok) {
renderSummaryError(data.error || '잔고 조회 실패');
return;
}
renderSummaryCards(data);
renderHoldings(data.stocks);
} catch (e) {
renderSummaryError('네트워크 오류: ' + e.message);
}
}
function renderSummaryCards(data) {
const plRate = parseFloat(data.totPrftRt || '0');
const plAmt = parseInt(data.totEvltPl || '0');
const plClass = plRate >= 0 ? 'text-red-500' : 'text-blue-500';
const cards = [
{
label: '추정예탁자산',
value: parseInt(data.prsmDpstAsetAmt || '0').toLocaleString('ko-KR') + '원',
cls: 'text-gray-800',
},
{
label: '총평가금액',
value: parseInt(data.totEvltAmt || '0').toLocaleString('ko-KR') + '원',
cls: 'text-gray-800',
},
{
label: '총평가손익',
value: (plAmt >= 0 ? '+' : '') + plAmt.toLocaleString('ko-KR') + '원',
cls: plClass,
},
{
label: '수익률',
value: (plRate >= 0 ? '+' : '') + plRate.toFixed(2) + '%',
cls: plClass,
},
];
document.getElementById('summaryCards').innerHTML = cards.map(c => `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<p class="text-xs text-gray-400 mb-1">${c.label}</p>
<p class="text-lg font-bold ${c.cls}">${c.value}</p>
</div>
`).join('');
}
function renderSummaryError(msg) {
document.getElementById('summaryCards').innerHTML = `
<div class="col-span-4 text-sm text-red-400 text-center py-4">${msg}</div>
`;
document.getElementById('holdingsTable').innerHTML = '';
}
function renderHoldings(stocks) {
const tbody = document.getElementById('holdingsTable');
if (!stocks || stocks.length === 0) {
tbody.innerHTML = '<div class="px-5 py-10 text-center text-sm text-gray-400">보유 종목이 없습니다.</div>';
return;
}
tbody.innerHTML = stocks.map(s => {
const prft = parseFloat(s.prftRt || '0');
const evlt = parseInt(s.evltvPrft || '0');
const cls = prft >= 0 ? 'text-red-500' : 'text-blue-500';
const sign = prft >= 0 ? '+' : '';
return `
<div class="grid grid-cols-[1fr_80px_90px_90px_100px_80px] text-sm px-5 py-3 border-b border-gray-50 hover:bg-gray-50 gap-2 items-center">
<a href="/stock/${s.stkCd}" class="font-medium text-gray-800 hover:text-blue-600 truncate">${s.stkNm}</a>
<span class="text-right text-gray-600">${parseInt(s.rmndQty || '0').toLocaleString('ko-KR')}주</span>
<span class="text-right text-gray-600">${parseInt(s.purPric || '0').toLocaleString('ko-KR')}</span>
<span class="text-right text-gray-600">${parseInt(s.curPrc || '0').toLocaleString('ko-KR')}</span>
<span class="text-right ${cls}">${(evlt >= 0 ? '+' : '') + evlt.toLocaleString('ko-KR')}원</span>
<span class="text-right ${cls}">${sign}${prft.toFixed(2)}%</span>
</div>`;
}).join('');
}
// -----------------------------------
// 예수금 카드 (GET /api/account/deposit — kt00001)
// -----------------------------------
async function loadCash() {
try {
const res = await fetch('/api/account/deposit');
const data = await res.json();
if (!res.ok) {
document.getElementById('cashEntr').textContent = '조회 실패';
document.getElementById('cashOrdAlowa').textContent = '조회 실패';
return;
}
// entr: 예수금, d2Entra: D+2 추정예수금, ordAlowAmt: 주문가능금액
document.getElementById('cashEntr').textContent =
data.d2Entra ? parseInt(data.d2Entra).toLocaleString('ko-KR') + '원' : '-';
document.getElementById('cashOrdAlowa').textContent =
data.ordAlowAmt ? parseInt(data.ordAlowAmt).toLocaleString('ko-KR') + '원' : '-';
} catch (e) {
document.getElementById('cashEntr').textContent = '오류';
document.getElementById('cashOrdAlowa').textContent = '오류';
}
}
// -----------------------------------
// 탭 전환
// -----------------------------------
function showAssetTab(tab) {
['pending', 'history'].forEach(t => {
const btn = document.getElementById('asset' + capitalize(t) + 'Tab');
const panel = document.getElementById('asset' + capitalize(t) + 'Panel');
const active = t === tab;
if (btn) {
btn.classList.toggle('border-b-2', active);
btn.classList.toggle('border-blue-500', active);
btn.classList.toggle('text-blue-600', active);
btn.classList.toggle('text-gray-500', !active);
}
if (panel) panel.classList.toggle('hidden', !active);
});
if (tab === 'pending') loadPending();
if (tab === 'history') loadHistory();
}
function capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
// -----------------------------------
// 미체결 목록 (GET /api/account/pending)
// -----------------------------------
async function loadPending() {
const panel = document.getElementById('assetPendingPanel');
if (!panel) return;
panel.innerHTML = '<div class="text-sm text-gray-400 text-center py-6 animate-pulse">조회 중...</div>';
try {
const res = await fetch('/api/account/pending');
const list = await res.json();
if (!res.ok) {
panel.innerHTML = `<div class="text-sm text-red-400 text-center py-6">${list.error || '조회 실패'}</div>`;
return;
}
if (!list || list.length === 0) {
panel.innerHTML = '<div class="text-sm text-gray-400 text-center py-6">미체결 주문이 없습니다.</div>';
return;
}
panel.innerHTML = `
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-xs text-gray-500 bg-gray-50 border-b border-gray-100">
<th class="px-3 py-2 text-left font-medium">종목명</th>
<th class="px-3 py-2 text-center font-medium">구분</th>
<th class="px-3 py-2 text-right font-medium">주문가</th>
<th class="px-3 py-2 text-right font-medium">미체결/주문</th>
<th class="px-3 py-2 text-center font-medium">취소</th>
</tr>
</thead>
<tbody>
${list.map(o => {
const isBuy = o.trdeTp === '2';
const cls = isBuy ? 'text-red-500' : 'text-blue-500';
const label = isBuy ? '매수' : '매도';
return `
<tr class="border-b border-gray-50 hover:bg-gray-50">
<td class="px-3 py-2.5 font-medium text-gray-800">${o.stkNm}</td>
<td class="px-3 py-2.5 text-center ${cls}">${label}</td>
<td class="px-3 py-2.5 text-right">${parseInt(o.ordPric || '0').toLocaleString('ko-KR')}원</td>
<td class="px-3 py-2.5 text-right">${parseInt(o.osoQty || '0').toLocaleString('ko-KR')} / ${parseInt(o.ordQty || '0').toLocaleString('ko-KR')}주</td>
<td class="px-3 py-2.5 text-center">
<button onclick="assetCancelOrder('${o.ordNo}','${o.stkCd}')"
class="px-2.5 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-100">취소</button>
</td>
</tr>`;
}).join('')}
</tbody>
</table>
</div>`;
} catch (e) {
panel.innerHTML = `<div class="text-sm text-red-400 text-center py-6">오류: ${e.message}</div>`;
}
}
// -----------------------------------
// 미체결 취소
// -----------------------------------
async function assetCancelOrder(ordNo, stkCd) {
if (!confirm('전량 취소하시겠습니까?')) return;
try {
const res = await fetch('/api/order', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ origOrdNo: ordNo, code: stkCd, qty: '0', exchange: 'KRX' }),
});
const data = await res.json();
if (!res.ok || data.error) {
showAssetToast(data.error || '취소 실패', 'error');
return;
}
showAssetToast('취소 주문 접수 완료', 'success');
loadPending();
} catch (e) {
showAssetToast('네트워크 오류', 'error');
}
}
// -----------------------------------
// 체결내역 (GET /api/account/history)
// -----------------------------------
async function loadHistory() {
const panel = document.getElementById('assetHistoryPanel');
if (!panel) return;
panel.innerHTML = '<div class="text-sm text-gray-400 text-center py-6 animate-pulse">조회 중...</div>';
try {
const res = await fetch('/api/account/history');
const list = await res.json();
if (!res.ok) {
panel.innerHTML = `<div class="text-sm text-red-400 text-center py-6">${list.error || '조회 실패'}</div>`;
return;
}
if (!list || list.length === 0) {
panel.innerHTML = '<div class="text-sm text-gray-400 text-center py-6">체결 내역이 없습니다.</div>';
return;
}
panel.innerHTML = `
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-xs text-gray-500 bg-gray-50 border-b border-gray-100">
<th class="px-3 py-2 text-left font-medium">종목명</th>
<th class="px-3 py-2 text-center font-medium">구분</th>
<th class="px-3 py-2 text-right font-medium">체결가</th>
<th class="px-3 py-2 text-right font-medium">체결수량</th>
<th class="px-3 py-2 text-right font-medium">수수료+세금</th>
</tr>
</thead>
<tbody>
${list.map(o => {
const isBuy = o.trdeTp === '2';
const cls = isBuy ? 'text-red-500' : 'text-blue-500';
const label = isBuy ? '매수' : '매도';
const fee = (parseInt(o.trdeCmsn || '0') + parseInt(o.trdeTax || '0')).toLocaleString('ko-KR');
return `
<tr class="border-b border-gray-50 hover:bg-gray-50">
<td class="px-3 py-2.5 font-medium text-gray-800">${o.stkNm}</td>
<td class="px-3 py-2.5 text-center ${cls}">${label}</td>
<td class="px-3 py-2.5 text-right">${parseInt(o.cntrPric || '0').toLocaleString('ko-KR')}원</td>
<td class="px-3 py-2.5 text-right">${parseInt(o.cntrQty || '0').toLocaleString('ko-KR')}주</td>
<td class="px-3 py-2.5 text-right text-gray-500">${fee}원</td>
</tr>`;
}).join('')}
</tbody>
</table>
</div>`;
} catch (e) {
panel.innerHTML = `<div class="text-sm text-red-400 text-center py-6">오류: ${e.message}</div>`;
}
}
// -----------------------------------
// 토스트 알림
// -----------------------------------
function showAssetToast(msg, type) {
const toast = document.createElement('div');
toast.className = `fixed bottom-6 left-1/2 -translate-x-1/2 z-50 px-5 py-3 rounded-lg text-sm font-medium shadow-lg transition-opacity
${type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'}`;
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 400);
}, 3000);
}