first commit
This commit is contained in:
487
static/js/order.js
Normal file
487
static/js/order.js
Normal file
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* 주문창 UI 전체 로직
|
||||
* - 매수/매도 탭, 주문유형, 단가/수량 입력, 주문 제출
|
||||
* - 미체결/체결/잔고 탭
|
||||
*/
|
||||
|
||||
// 현재 활성 탭: 'buy' | 'sell'
|
||||
let orderSide = 'buy';
|
||||
// 주문가능수량 (퍼센트 버튼용)
|
||||
let orderableQty = 0;
|
||||
// 현재 종목 코드 (전역에서 STOCK_CODE 사용)
|
||||
|
||||
// -----------------------------------
|
||||
// 호가 단위(틱) 계산
|
||||
// -----------------------------------
|
||||
function getTickSize(price) {
|
||||
if (price < 2000) return 1;
|
||||
if (price < 5000) return 5;
|
||||
if (price < 20000) return 10;
|
||||
if (price < 50000) return 50;
|
||||
if (price < 200000) return 100;
|
||||
if (price < 500000) return 500;
|
||||
return 1000;
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 초기화
|
||||
// -----------------------------------
|
||||
function initOrder() {
|
||||
updateOrderSide('buy');
|
||||
loadOrderable();
|
||||
loadPendingTab();
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 매수/매도 탭 전환
|
||||
// -----------------------------------
|
||||
function updateOrderSide(side) {
|
||||
orderSide = side;
|
||||
|
||||
const buyTab = document.getElementById('orderBuyTab');
|
||||
const sellTab = document.getElementById('orderSellTab');
|
||||
const submitBtn = document.getElementById('orderSubmitBtn');
|
||||
|
||||
if (side === 'buy') {
|
||||
buyTab.classList.add('bg-red-500', 'text-white');
|
||||
buyTab.classList.remove('bg-gray-100', 'text-gray-600');
|
||||
sellTab.classList.add('bg-gray-100', 'text-gray-600');
|
||||
sellTab.classList.remove('bg-blue-500', 'text-white');
|
||||
submitBtn.textContent = '매수';
|
||||
submitBtn.className = 'w-full py-2.5 text-sm font-bold rounded-lg bg-red-500 hover:bg-red-600 text-white transition-colors';
|
||||
} else {
|
||||
sellTab.classList.add('bg-blue-500', 'text-white');
|
||||
sellTab.classList.remove('bg-gray-100', 'text-gray-600');
|
||||
buyTab.classList.add('bg-gray-100', 'text-gray-600');
|
||||
buyTab.classList.remove('bg-red-500', 'text-white');
|
||||
submitBtn.textContent = '매도';
|
||||
submitBtn.className = 'w-full py-2.5 text-sm font-bold rounded-lg bg-blue-500 hover:bg-blue-600 text-white transition-colors';
|
||||
}
|
||||
|
||||
// 폼 초기화
|
||||
resetOrderForm();
|
||||
loadOrderable();
|
||||
}
|
||||
|
||||
function resetOrderForm() {
|
||||
const priceEl = document.getElementById('orderPrice');
|
||||
const qtyEl = document.getElementById('orderQty');
|
||||
if (priceEl) priceEl.value = '';
|
||||
if (qtyEl) qtyEl.value = '';
|
||||
updateOrderTotal();
|
||||
hideOrderConfirm();
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 주문유형 변경 처리
|
||||
// -----------------------------------
|
||||
function onTradeTypeChange() {
|
||||
const tp = document.getElementById('orderTradeType').value;
|
||||
const priceEl = document.getElementById('orderPrice');
|
||||
const priceUpBtn = document.getElementById('priceUpBtn');
|
||||
const priceDownBtn = document.getElementById('priceDownBtn');
|
||||
|
||||
// 시장가(3)일 때 단가 입력 비활성화
|
||||
const isMarket = (tp === '3');
|
||||
priceEl.disabled = isMarket;
|
||||
priceUpBtn.disabled = isMarket;
|
||||
priceDownBtn.disabled = isMarket;
|
||||
if (isMarket) {
|
||||
priceEl.value = '';
|
||||
priceEl.placeholder = '시장가';
|
||||
updateOrderTotal();
|
||||
} else {
|
||||
priceEl.placeholder = '단가 입력';
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 단가 ▲▼ 버튼
|
||||
// -----------------------------------
|
||||
function adjustPrice(direction) {
|
||||
const el = document.getElementById('orderPrice');
|
||||
// 입력란이 비어 있으면 현재가를 기준으로 시작
|
||||
let price = parseInt(el.value, 10);
|
||||
if (!price || isNaN(price)) {
|
||||
const rawEl = document.getElementById('currentPrice');
|
||||
price = rawEl ? parseInt(rawEl.dataset.raw || '0', 10) : 0;
|
||||
}
|
||||
const tick = getTickSize(price);
|
||||
price += direction * tick;
|
||||
if (price < 1) price = 1;
|
||||
el.value = price;
|
||||
updateOrderTotal();
|
||||
loadOrderable();
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 호가창 클릭 → 단가 자동 입력
|
||||
// -----------------------------------
|
||||
window.setOrderPrice = function(price) {
|
||||
const priceEl = document.getElementById('orderPrice');
|
||||
if (!priceEl || priceEl.disabled) return;
|
||||
priceEl.value = price;
|
||||
updateOrderTotal();
|
||||
loadOrderable();
|
||||
};
|
||||
|
||||
// -----------------------------------
|
||||
// 총 주문금액 실시간 표시
|
||||
// -----------------------------------
|
||||
function updateOrderTotal() {
|
||||
const price = parseInt(document.getElementById('orderPrice').value.replace(/,/g, ''), 10) || 0;
|
||||
const qty = parseInt(document.getElementById('orderQty').value.replace(/,/g, ''), 10) || 0;
|
||||
const total = price * qty;
|
||||
const el = document.getElementById('orderTotal');
|
||||
if (el) el.textContent = total > 0 ? total.toLocaleString('ko-KR') + '원' : '-';
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 수량 퍼센트 버튼
|
||||
// -----------------------------------
|
||||
function setQtyPercent(pct) {
|
||||
if (orderableQty <= 0) return;
|
||||
const qty = Math.floor(orderableQty * pct / 100);
|
||||
const el = document.getElementById('orderQty');
|
||||
if (el) el.value = qty > 0 ? qty : 0;
|
||||
updateOrderTotal();
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 주문가능금액/수량 조회
|
||||
// -----------------------------------
|
||||
async function loadOrderable() {
|
||||
const code = typeof STOCK_CODE !== 'undefined' ? STOCK_CODE : '';
|
||||
const price = document.getElementById('orderPrice')?.value || '0';
|
||||
const side = orderSide === 'buy' ? 'buy' : 'sell';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/account/orderable?code=${code}&price=${price}&side=${side}`);
|
||||
const data = await res.json();
|
||||
|
||||
const qtyEl = document.getElementById('orderableQty');
|
||||
const amtEl = document.getElementById('orderableAmt');
|
||||
const entrEl = document.getElementById('orderableEntr');
|
||||
|
||||
if (data.ordAlowq) {
|
||||
orderableQty = parseInt(data.ordAlowq.replace(/,/g, ''), 10) || 0;
|
||||
if (qtyEl) qtyEl.textContent = orderableQty.toLocaleString('ko-KR') + '주';
|
||||
} else {
|
||||
orderableQty = 0;
|
||||
if (qtyEl) qtyEl.textContent = '-';
|
||||
}
|
||||
|
||||
if (amtEl) amtEl.textContent = data.ordAlowa ? parseInt(data.ordAlowa.replace(/,/g, ''), 10).toLocaleString('ko-KR') + '원' : '-';
|
||||
if (entrEl) entrEl.textContent = data.entr ? parseInt(data.entr.replace(/,/g, ''), 10).toLocaleString('ko-KR') + '원' : '-';
|
||||
} catch (e) {
|
||||
// 조회 실패 시 조용히 처리
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 주문 확인 메시지 표시/숨김
|
||||
// -----------------------------------
|
||||
function showOrderConfirm() {
|
||||
const price = parseInt(document.getElementById('orderPrice').value.replace(/,/g, ''), 10) || 0;
|
||||
const qty = parseInt(document.getElementById('orderQty').value.replace(/,/g, ''), 10) || 0;
|
||||
const tp = document.getElementById('orderTradeType').value;
|
||||
const name = typeof STOCK_NAME !== 'undefined' ? STOCK_NAME : '';
|
||||
|
||||
if (qty <= 0) {
|
||||
showOrderToast('수량을 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
if (tp !== '3' && price <= 0) {
|
||||
showOrderToast('단가를 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const sideText = orderSide === 'buy' ? '매수' : '매도';
|
||||
const priceText = tp === '3' ? '시장가' : price.toLocaleString('ko-KR') + '원';
|
||||
const msg = `${name} ${qty.toLocaleString('ko-KR')}주 ${priceText} ${sideText} 하시겠습니까?`;
|
||||
|
||||
const confirmEl = document.getElementById('orderConfirmMsg');
|
||||
const confirmBox = document.getElementById('orderConfirmBox');
|
||||
if (confirmEl) confirmEl.textContent = msg;
|
||||
if (confirmBox) confirmBox.classList.remove('hidden');
|
||||
document.getElementById('orderSubmitBtn').classList.add('hidden');
|
||||
}
|
||||
|
||||
function hideOrderConfirm() {
|
||||
const confirmBox = document.getElementById('orderConfirmBox');
|
||||
const submitBtn = document.getElementById('orderSubmitBtn');
|
||||
if (confirmBox) confirmBox.classList.add('hidden');
|
||||
if (submitBtn) submitBtn.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 주문 제출
|
||||
// -----------------------------------
|
||||
async function submitOrder() {
|
||||
const price = document.getElementById('orderPrice').value || '';
|
||||
const qty = document.getElementById('orderQty').value || '';
|
||||
const tradeTP = document.getElementById('orderTradeType').value;
|
||||
const exchange = document.querySelector('input[name="orderExchange"]:checked')?.value || 'KRX';
|
||||
const code = typeof STOCK_CODE !== 'undefined' ? STOCK_CODE : '';
|
||||
|
||||
hideOrderConfirm();
|
||||
|
||||
const payload = {
|
||||
exchange: exchange,
|
||||
code: code,
|
||||
qty: qty.replace(/,/g, ''),
|
||||
price: tradeTP === '3' ? '' : price.replace(/,/g, ''),
|
||||
tradeTP: tradeTP,
|
||||
};
|
||||
|
||||
const url = orderSide === 'buy' ? '/api/order/buy' : '/api/order/sell';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.error) {
|
||||
showOrderToast(data.error || '주문 실패', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const sideText = orderSide === 'buy' ? '매수' : '매도';
|
||||
showOrderToast(`${sideText} 주문 접수 완료 (주문번호: ${data.orderNo})`, 'success');
|
||||
resetOrderForm();
|
||||
loadOrderable();
|
||||
// 미체결 탭 갱신
|
||||
loadPendingTab();
|
||||
if (document.getElementById('pendingTab')?.classList.contains('active-tab')) {
|
||||
showAccountTab('pending');
|
||||
}
|
||||
} catch (e) {
|
||||
showOrderToast('네트워크 오류: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 토스트 알림
|
||||
// -----------------------------------
|
||||
function showOrderToast(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);
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 계좌 탭 전환
|
||||
// -----------------------------------
|
||||
function showAccountTab(tab) {
|
||||
['pending', 'history', 'balance'].forEach(t => {
|
||||
const btn = document.getElementById(t + 'Tab');
|
||||
const panel = document.getElementById(t + 'Panel');
|
||||
if (btn) {
|
||||
if (t === tab) {
|
||||
btn.classList.add('border-b-2', 'border-blue-500', 'text-blue-600', 'active-tab');
|
||||
btn.classList.remove('text-gray-500');
|
||||
} else {
|
||||
btn.classList.remove('border-b-2', 'border-blue-500', 'text-blue-600', 'active-tab');
|
||||
btn.classList.add('text-gray-500');
|
||||
}
|
||||
}
|
||||
if (panel) panel.classList.toggle('hidden', t !== tab);
|
||||
});
|
||||
|
||||
if (tab === 'pending') loadPendingTab();
|
||||
if (tab === 'history') loadHistoryTab();
|
||||
if (tab === 'balance') loadBalanceTab();
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 미체결 탭
|
||||
// -----------------------------------
|
||||
async function loadPendingTab() {
|
||||
const panel = document.getElementById('pendingPanel');
|
||||
if (!panel) return;
|
||||
|
||||
panel.innerHTML = '<p class="text-xs text-gray-400 text-center py-4">조회 중...</p>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/account/pending');
|
||||
const list = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
panel.innerHTML = `<p class="text-xs text-red-400 text-center py-4">${list.error || '조회 실패'}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
panel.innerHTML = '<p class="text-xs text-gray-400 text-center py-4">미체결 내역이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
panel.innerHTML = list.map(o => {
|
||||
const isBuy = o.trdeTp === '2';
|
||||
const sideClass = isBuy ? 'text-red-500' : 'text-blue-500';
|
||||
const sideText = isBuy ? '매수' : '매도';
|
||||
return `
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-100 text-xs gap-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-semibold text-gray-800 truncate">${o.stkNm}</p>
|
||||
<p class="${sideClass}">${sideText} · ${parseInt(o.ordPric||0).toLocaleString('ko-KR')}원</p>
|
||||
<p class="text-gray-400">미체결 ${parseInt(o.osoQty||0).toLocaleString('ko-KR')}/${parseInt(o.ordQty||0).toLocaleString('ko-KR')}주</p>
|
||||
</div>
|
||||
<div class="flex gap-1 shrink-0">
|
||||
<button onclick="cancelOrder('${o.ordNo}','${o.stkCd}')"
|
||||
class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-100">취소</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<p class="text-xs text-red-400 text-center py-4">오류: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 미체결 취소
|
||||
// -----------------------------------
|
||||
async function cancelOrder(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) {
|
||||
showOrderToast(data.error || '취소 실패', 'error');
|
||||
return;
|
||||
}
|
||||
showOrderToast('취소 주문 접수 완료', 'success');
|
||||
loadPendingTab();
|
||||
} catch (e) {
|
||||
showOrderToast('네트워크 오류', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 체결내역 탭
|
||||
// -----------------------------------
|
||||
async function loadHistoryTab() {
|
||||
const panel = document.getElementById('historyPanel');
|
||||
if (!panel) return;
|
||||
|
||||
panel.innerHTML = '<p class="text-xs text-gray-400 text-center py-4">조회 중...</p>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/account/history');
|
||||
const list = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
panel.innerHTML = `<p class="text-xs text-red-400 text-center py-4">${list.error || '조회 실패'}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
panel.innerHTML = '<p class="text-xs text-gray-400 text-center py-4">체결 내역이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
panel.innerHTML = list.map(o => {
|
||||
const isBuy = o.trdeTp === '2';
|
||||
const sideClass = isBuy ? 'text-red-500' : 'text-blue-500';
|
||||
const sideText = isBuy ? '매수' : '매도';
|
||||
const fee = (parseInt(o.trdeCmsn||0) + parseInt(o.trdeTax||0)).toLocaleString('ko-KR');
|
||||
return `
|
||||
<div class="py-2 border-b border-gray-100 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-semibold text-gray-800">${o.stkNm}</span>
|
||||
<span class="${sideClass}">${sideText}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-gray-500 mt-0.5">
|
||||
<span>체결가 ${parseInt(o.cntrPric||0).toLocaleString('ko-KR')}원 × ${parseInt(o.cntrQty||0).toLocaleString('ko-KR')}주</span>
|
||||
<span>수수료+세금 ${fee}원</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<p class="text-xs text-red-400 text-center py-4">오류: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// 잔고 탭
|
||||
// -----------------------------------
|
||||
async function loadBalanceTab() {
|
||||
const panel = document.getElementById('balancePanel');
|
||||
if (!panel) return;
|
||||
|
||||
panel.innerHTML = '<p class="text-xs text-gray-400 text-center py-4">조회 중...</p>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/account/balance');
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
panel.innerHTML = `<p class="text-xs text-red-400 text-center py-4">${data.error || '조회 실패'}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const plClass = parseFloat(data.totPrftRt || '0') >= 0 ? 'text-red-500' : 'text-blue-500';
|
||||
let html = `
|
||||
<div class="grid grid-cols-2 gap-2 text-xs mb-3 pb-2 border-b border-gray-100">
|
||||
<div>
|
||||
<p class="text-gray-400">추정예탁자산</p>
|
||||
<p class="font-semibold">${parseInt(data.prsmDpstAsetAmt||0).toLocaleString('ko-KR')}원</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">총평가손익</p>
|
||||
<p class="font-semibold ${plClass}">${parseInt(data.totEvltPl||0).toLocaleString('ko-KR')}원</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">총평가금액</p>
|
||||
<p class="font-semibold">${parseInt(data.totEvltAmt||0).toLocaleString('ko-KR')}원</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">수익률</p>
|
||||
<p class="font-semibold ${plClass}">${parseFloat(data.totPrftRt||0).toFixed(2)}%</p>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (!data.stocks || data.stocks.length === 0) {
|
||||
html += '<p class="text-xs text-gray-400 text-center py-2">보유 종목이 없습니다.</p>';
|
||||
} else {
|
||||
html += data.stocks.map(s => {
|
||||
const prft = parseFloat(s.prftRt || '0');
|
||||
const cls = prft >= 0 ? 'text-red-500' : 'text-blue-500';
|
||||
return `
|
||||
<div class="py-2 border-b border-gray-100 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-semibold text-gray-800">${s.stkNm}</span>
|
||||
<span class="${cls}">${prft >= 0 ? '+' : ''}${prft.toFixed(2)}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-gray-500 mt-0.5">
|
||||
<span>${parseInt(s.rmndQty||0).toLocaleString('ko-KR')}주 / 평단 ${parseInt(s.purPric||0).toLocaleString('ko-KR')}원</span>
|
||||
<span class="${cls}">${parseInt(s.evltvPrft||0).toLocaleString('ko-KR')}원</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
panel.innerHTML = html;
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<p class="text-xs text-red-400 text-center py-4">오류: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// DOM 준비 후 초기화
|
||||
// -----------------------------------
|
||||
document.addEventListener('DOMContentLoaded', initOrder);
|
||||
Reference in New Issue
Block a user