first commit
This commit is contained in:
195
static/js/kospi200.js
Normal file
195
static/js/kospi200.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 코스피200 종목 목록 페이지
|
||||
* - /api/kospi200 폴링 (1분 캐시 기반)
|
||||
* - 정렬: 등락률 / 거래량 / 현재가
|
||||
* - 필터: 전체 / 상승 / 하락
|
||||
* - 종목명 검색
|
||||
*/
|
||||
(function () {
|
||||
let allStocks = [];
|
||||
let currentSort = 'fluRt'; // fluRt | volume | curPrc
|
||||
let currentDir = 'all'; // all | up | down
|
||||
let sortDesc = true; // 기본 내림차순
|
||||
|
||||
const listEl = document.getElementById('k200List');
|
||||
const countEl = document.getElementById('k200Count');
|
||||
const searchEl = document.getElementById('k200Search');
|
||||
const updatedEl = document.getElementById('lastUpdated');
|
||||
|
||||
// ── 포맷 유틸 ────────────────────────────────────────────────
|
||||
function fmtNum(n) {
|
||||
if (n == null || n === 0) 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 fmtDiff(n) {
|
||||
if (n == null || n === 0) return '0';
|
||||
const sign = n > 0 ? '+' : '';
|
||||
return sign + Math.abs(n).toLocaleString('ko-KR');
|
||||
}
|
||||
function rateClass(f) {
|
||||
if (f > 0) return 'text-red-500';
|
||||
if (f < 0) return 'text-blue-500';
|
||||
return 'text-gray-500';
|
||||
}
|
||||
function rateBg(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 makeRow(s, rank) {
|
||||
const cls = rateClass(s.fluRt);
|
||||
const bgCls = rateBg(s.fluRt);
|
||||
return `
|
||||
<a href="/stock/${s.code}"
|
||||
class="grid grid-cols-[2.5rem_1fr_1fr_90px_90px_100px_90px_90px_90px] gap-2
|
||||
px-4 py-2.5 hover:bg-gray-50 transition-colors text-sm items-center">
|
||||
<span class="text-xs text-gray-400 text-center">${rank}</span>
|
||||
<div class="min-w-0">
|
||||
<p class="font-medium text-gray-800 truncate">${s.name}</p>
|
||||
<p class="text-xs text-gray-400 font-mono">${s.code}</p>
|
||||
</div>
|
||||
<div class="text-right font-semibold ${cls}">${fmtNum(s.curPrc)}원</div>
|
||||
<div class="text-right text-xs ${cls}">${fmtDiff(s.predPre)}</div>
|
||||
<div class="text-right">
|
||||
<span class="text-xs px-1.5 py-0.5 rounded font-semibold ${bgCls}">${fmtRate(s.fluRt)}</span>
|
||||
</div>
|
||||
<div class="text-right text-xs text-gray-500">${fmtNum(s.volume)}</div>
|
||||
<div class="text-right text-xs text-gray-400">${fmtNum(s.open)}</div>
|
||||
<div class="text-right text-xs text-red-400">${fmtNum(s.high)}</div>
|
||||
<div class="text-right text-xs text-blue-400">${fmtNum(s.low)}</div>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
// ── 필터 + 정렬 + 렌더 ───────────────────────────────────────
|
||||
function renderList() {
|
||||
const q = searchEl.value.trim();
|
||||
|
||||
let filtered = allStocks;
|
||||
|
||||
// 상승/하락 필터
|
||||
if (currentDir === 'up') filtered = filtered.filter(s => s.fluRt > 0);
|
||||
if (currentDir === 'down') filtered = filtered.filter(s => s.fluRt < 0);
|
||||
|
||||
// 종목명 검색
|
||||
if (q) filtered = filtered.filter(s => s.name.includes(q) || s.code.includes(q));
|
||||
|
||||
// 정렬
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
const diff = b[currentSort] - a[currentSort];
|
||||
return sortDesc ? diff : -diff;
|
||||
});
|
||||
|
||||
countEl.textContent = `${filtered.length}개 종목`;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
listEl.innerHTML = `<div class="px-4 py-10 text-center text-sm text-gray-400">조건에 맞는 종목이 없습니다.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = filtered.map((s, i) => makeRow(s, i + 1)).join('');
|
||||
}
|
||||
|
||||
// ── 데이터 로드 ───────────────────────────────────────────────
|
||||
async function loadData() {
|
||||
try {
|
||||
const resp = await fetch('/api/kospi200');
|
||||
if (!resp.ok) throw new Error('조회 실패');
|
||||
allStocks = await resp.json();
|
||||
updatedEl.textContent = new Date().toTimeString().slice(0, 8) + ' 기준';
|
||||
renderList();
|
||||
} catch (e) {
|
||||
listEl.innerHTML = `<div class="px-4 py-10 text-center text-sm text-red-400">데이터를 불러오지 못했습니다.</div>`;
|
||||
console.error('코스피200 조회 실패:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 헤더 정렬 화살표 갱신 ────────────────────────────────────
|
||||
function updateColHeaders() {
|
||||
document.querySelectorAll('.col-sort').forEach(el => {
|
||||
const arrow = el.querySelector('.sort-arrow');
|
||||
if (!arrow) return;
|
||||
if (el.dataset.col === currentSort) {
|
||||
arrow.textContent = sortDesc ? '▼' : '▲';
|
||||
arrow.className = 'sort-arrow text-blue-400';
|
||||
el.classList.add('text-blue-500');
|
||||
el.classList.remove('text-gray-500');
|
||||
} else {
|
||||
arrow.textContent = '';
|
||||
arrow.className = 'sort-arrow';
|
||||
el.classList.remove('text-blue-500');
|
||||
el.classList.add('text-gray-500');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── 정렬 탭 이벤트 ───────────────────────────────────────────
|
||||
document.querySelectorAll('.sort-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (currentSort === btn.dataset.sort) {
|
||||
sortDesc = !sortDesc;
|
||||
} else {
|
||||
currentSort = btn.dataset.sort;
|
||||
sortDesc = true;
|
||||
}
|
||||
document.querySelectorAll('.sort-tab').forEach(b => {
|
||||
const active = b.dataset.sort === currentSort;
|
||||
b.className = active
|
||||
? 'sort-tab px-3 py-1.5 bg-blue-500 text-white text-xs font-medium'
|
||||
: 'sort-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200 text-xs font-medium';
|
||||
});
|
||||
updateColHeaders();
|
||||
renderList();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 헤더 컬럼 클릭 정렬 ──────────────────────────────────────
|
||||
document.querySelectorAll('.col-sort').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const col = el.dataset.col;
|
||||
if (currentSort === col) {
|
||||
sortDesc = !sortDesc;
|
||||
} else {
|
||||
currentSort = col;
|
||||
sortDesc = true;
|
||||
}
|
||||
// 상단 정렬 탭 동기화
|
||||
document.querySelectorAll('.sort-tab').forEach(b => {
|
||||
const active = b.dataset.sort === currentSort;
|
||||
b.className = active
|
||||
? 'sort-tab px-3 py-1.5 bg-blue-500 text-white text-xs font-medium'
|
||||
: 'sort-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200 text-xs font-medium';
|
||||
});
|
||||
updateColHeaders();
|
||||
renderList();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 방향 필터 탭 이벤트 ──────────────────────────────────────
|
||||
document.querySelectorAll('.dir-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
currentDir = btn.dataset.dir;
|
||||
document.querySelectorAll('.dir-tab').forEach(b => {
|
||||
const active = b.dataset.dir === currentDir;
|
||||
b.className = active
|
||||
? 'dir-tab px-3 py-1.5 bg-blue-500 text-white text-xs font-medium'
|
||||
: 'dir-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200 text-xs font-medium';
|
||||
});
|
||||
renderList();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 검색 이벤트 ──────────────────────────────────────────────
|
||||
searchEl.addEventListener('input', renderList);
|
||||
|
||||
// ── 초기 로드 + 1분 자동 갱신 ────────────────────────────────
|
||||
updateColHeaders();
|
||||
loadData();
|
||||
setInterval(loadData, 60 * 1000);
|
||||
})();
|
||||
Reference in New Issue
Block a user