first commit
This commit is contained in:
140
static/js/ranking.js
Normal file
140
static/js/ranking.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 상승률 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);
|
||||
})();
|
||||
Reference in New Issue
Block a user