/** * 주문창 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 = '
조회 중...
'; try { const res = await fetch('/api/account/pending'); const list = await res.json(); if (!res.ok) { panel.innerHTML = `${list.error || '조회 실패'}
`; return; } if (!list || list.length === 0) { panel.innerHTML = '미체결 내역이 없습니다.
'; return; } panel.innerHTML = list.map(o => { const isBuy = o.trdeTp === '2'; const sideClass = isBuy ? 'text-red-500' : 'text-blue-500'; const sideText = isBuy ? '매수' : '매도'; return `${o.stkNm}
${sideText} · ${parseInt(o.ordPric||0).toLocaleString('ko-KR')}원
미체결 ${parseInt(o.osoQty||0).toLocaleString('ko-KR')}/${parseInt(o.ordQty||0).toLocaleString('ko-KR')}주
오류: ${e.message}
`; } } // ----------------------------------- // 미체결 취소 // ----------------------------------- 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 = '조회 중...
'; try { const res = await fetch('/api/account/history'); const list = await res.json(); if (!res.ok) { panel.innerHTML = `${list.error || '조회 실패'}
`; return; } if (!list || list.length === 0) { panel.innerHTML = '체결 내역이 없습니다.
'; 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 `오류: ${e.message}
`; } } // ----------------------------------- // 잔고 탭 // ----------------------------------- async function loadBalanceTab() { const panel = document.getElementById('balancePanel'); if (!panel) return; panel.innerHTML = '조회 중...
'; try { const res = await fetch('/api/account/balance'); const data = await res.json(); if (!res.ok) { panel.innerHTML = `${data.error || '조회 실패'}
`; return; } const plClass = parseFloat(data.totPrftRt || '0') >= 0 ? 'text-red-500' : 'text-blue-500'; let html = `추정예탁자산
${parseInt(data.prsmDpstAsetAmt||0).toLocaleString('ko-KR')}원
총평가손익
${parseInt(data.totEvltPl||0).toLocaleString('ko-KR')}원
총평가금액
${parseInt(data.totEvltAmt||0).toLocaleString('ko-KR')}원
수익률
${parseFloat(data.totPrftRt||0).toFixed(2)}%
보유 종목이 없습니다.
'; } else { html += data.stocks.map(s => { const prft = parseFloat(s.prftRt || '0'); const cls = prft >= 0 ? 'text-red-500' : 'text-blue-500'; return `오류: ${e.message}
`; } } // ----------------------------------- // DOM 준비 후 초기화 // ----------------------------------- document.addEventListener('DOMContentLoaded', initOrder);