309 lines
13 KiB
JavaScript
309 lines
13 KiB
JavaScript
/**
|
|
* 자산 현황 페이지 로직
|
|
* - 요약 카드, 예수금, 보유 종목, 미체결/체결내역
|
|
*/
|
|
|
|
// -----------------------------------
|
|
// 초기화
|
|
// -----------------------------------
|
|
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);
|
|
}
|