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

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