/** * 상승률 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 `
${s.code} ${fmtRate(s.changeRate)}

${s.name}

${fmtNum(s.currentPrice)}원

거래량 ${fmtNum(s.volume)} 체결강도 ${fmtCntr(s.cntrStr)}
`; } // --- 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 = '

데이터가 없습니다.

'; 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); })();