140 lines
5.5 KiB
JavaScript
140 lines
5.5 KiB
JavaScript
/**
|
|
* 상승률 TOP 10 실시간 갱신
|
|
* - 페이지 로드 시 즉시 조회 + WS 개별 구독
|
|
* - 30초마다 랭킹 순위 재조회 (종목 변동 반영)
|
|
* - WS로 현재가/등락률/거래량/체결강도 1초 단위 갱신
|
|
*/
|
|
(function () {
|
|
const gridEl = document.getElementById('rankingGrid');
|
|
const updatedEl = document.getElementById('rankingUpdatedAt');
|
|
const INTERVAL = 30 * 1000; // 30초 (순위 재조회 주기)
|
|
|
|
// 현재 구독 중인 종목 코드 목록 (재조회 시 해제용)
|
|
let currentCodes = [];
|
|
|
|
// --- 숫자 포맷 ---
|
|
function fmtNum(n) {
|
|
if (n == null) return '-';
|
|
return Math.abs(n).toLocaleString('ko-KR');
|
|
}
|
|
function fmtRate(f) {
|
|
if (f == null) return '-';
|
|
const sign = f >= 0 ? '+' : '';
|
|
return sign + f.toFixed(2) + '%';
|
|
}
|
|
function fmtCntr(f) {
|
|
if (!f) return '-';
|
|
return f.toFixed(2);
|
|
}
|
|
|
|
// --- 등락률에 따른 CSS 클래스 ---
|
|
function rateClass(f) {
|
|
if (f > 0) return 'text-red-500';
|
|
if (f < 0) return 'text-blue-500';
|
|
return 'text-gray-500';
|
|
}
|
|
function rateBadgeClass(f) {
|
|
if (f > 0) return 'bg-red-50 text-red-500';
|
|
if (f < 0) return 'bg-blue-50 text-blue-500';
|
|
return 'bg-gray-100 text-gray-500';
|
|
}
|
|
function cntrClass(f) {
|
|
if (f > 100) return 'text-red-500';
|
|
if (f > 0 && f < 100) return 'text-blue-500';
|
|
return 'text-gray-400';
|
|
}
|
|
|
|
// --- 카드 HTML 생성 (실시간 업데이트용 ID 포함) ---
|
|
function makeCard(s) {
|
|
const colorCls = rateClass(s.changeRate);
|
|
return `
|
|
<a href="/stock/${s.code}" id="rk-${s.code}"
|
|
class="block bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow p-4 border border-gray-100">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-xs text-gray-400 font-mono">${s.code}</span>
|
|
<span id="rk-rate-${s.code}" class="text-xs px-2 py-0.5 rounded-full font-semibold ${rateBadgeClass(s.changeRate)}">
|
|
${fmtRate(s.changeRate)}
|
|
</span>
|
|
</div>
|
|
<p class="font-semibold text-gray-800 text-sm truncate mb-1">${s.name}</p>
|
|
<p id="rk-price-${s.code}" class="text-lg font-bold ${colorCls}">${fmtNum(s.currentPrice)}원</p>
|
|
<div class="flex justify-between text-xs mt-1">
|
|
<span class="text-gray-400">거래량 <span id="rk-vol-${s.code}">${fmtNum(s.volume)}</span></span>
|
|
<span id="rk-cntr-${s.code}" class="${cntrClass(s.cntrStr)}">체결강도 ${fmtCntr(s.cntrStr)}</span>
|
|
</div>
|
|
</a>`;
|
|
}
|
|
|
|
// --- WS 실시간 카드 업데이트 ---
|
|
function updateCard(code, data) {
|
|
const priceEl = document.getElementById(`rk-price-${code}`);
|
|
const rateEl = document.getElementById(`rk-rate-${code}`);
|
|
const volEl = document.getElementById(`rk-vol-${code}`);
|
|
const cntrEl = document.getElementById(`rk-cntr-${code}`);
|
|
if (!priceEl) return;
|
|
|
|
const rate = data.changeRate ?? 0;
|
|
const colorCls = rateClass(rate);
|
|
const sign = rate >= 0 ? '+' : '';
|
|
|
|
priceEl.textContent = fmtNum(data.currentPrice) + '원';
|
|
priceEl.className = `text-lg font-bold ${colorCls}`;
|
|
rateEl.textContent = sign + rate.toFixed(2) + '%';
|
|
rateEl.className = `text-xs px-2 py-0.5 rounded-full font-semibold ${rateBadgeClass(rate)}`;
|
|
volEl.textContent = fmtNum(data.volume);
|
|
if (cntrEl && data.cntrStr !== undefined) {
|
|
cntrEl.textContent = '체결강도 ' + fmtCntr(data.cntrStr);
|
|
cntrEl.className = cntrClass(data.cntrStr);
|
|
}
|
|
}
|
|
|
|
// --- 이전 구독 해제 후 새 종목 구독 ---
|
|
function resubscribe(newCodes) {
|
|
// 빠진 종목 구독 해제
|
|
currentCodes.forEach(code => {
|
|
if (!newCodes.includes(code)) {
|
|
stockWS.unsubscribe(code);
|
|
}
|
|
});
|
|
// 새로 추가된 종목 구독
|
|
newCodes.forEach(code => {
|
|
if (!currentCodes.includes(code)) {
|
|
stockWS.subscribe(code);
|
|
stockWS.onPrice(code, data => updateCard(code, data));
|
|
}
|
|
});
|
|
currentCodes = newCodes;
|
|
}
|
|
|
|
// --- 랭킹 조회 및 렌더링 ---
|
|
async function fetchAndRender() {
|
|
try {
|
|
const resp = await fetch('/api/ranking?market=J&dir=up');
|
|
if (!resp.ok) throw new Error('조회 실패');
|
|
const stocks = await resp.json();
|
|
|
|
if (!Array.isArray(stocks) || stocks.length === 0) {
|
|
gridEl.innerHTML = '<p class="col-span-5 text-gray-400 text-center py-8">데이터가 없습니다.</p>';
|
|
return;
|
|
}
|
|
|
|
gridEl.innerHTML = stocks.map(makeCard).join('');
|
|
|
|
// WS 구독 갱신
|
|
resubscribe(stocks.map(s => s.code));
|
|
|
|
// 순위 갱신 시각 표시
|
|
const now = new Date();
|
|
const hh = String(now.getHours()).padStart(2, '0');
|
|
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
updatedEl.textContent = `${hh}:${mm}:${ss} 기준`;
|
|
} catch (e) {
|
|
console.error('랭킹 조회 실패:', e);
|
|
}
|
|
}
|
|
|
|
// 초기 로드 + 30초마다 순위 재조회
|
|
fetchAndRender();
|
|
setInterval(fetchAndRender, INTERVAL);
|
|
})(); |