first commit

This commit is contained in:
hayato5246
2026-03-31 19:32:59 +09:00
commit d10b794c9f
78 changed files with 1671595 additions and 0 deletions

140
static/js/ranking.js Normal file
View 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);
})();