/** * 테마 분석 페이지 * - 테마 목록 조회 (ka90001) * - 테마 구성종목 조회 (ka90002) */ (function () { let currentDate = '1'; let currentSort = '3'; // 3=등락률순, 1=기간수익률순 let selectedCode = null; let selectedName = null; let allThemes = []; let clientSortCol = null; // 헤더 클릭 정렬 컬럼 (null=서버 순서 유지) let clientSortDesc = true; const listEl = document.getElementById('themeList'); const countEl = document.getElementById('themeCount'); const searchEl = document.getElementById('themeSearch'); const emptyEl = document.getElementById('themeDetailEmpty'); const contentEl = document.getElementById('themeDetailContent'); const loadingEl = document.getElementById('themeDetailLoading'); const nameEl = document.getElementById('detailThemeName'); const fluRtEl = document.getElementById('detailFluRt'); const periodRtEl = document.getElementById('detailPeriodRt'); const stockListEl = document.getElementById('detailStockList'); // ── 포맷 유틸 ──────────────────────────────────────────────── function fmtRate(f) { if (f == null) return '-'; const sign = f >= 0 ? '+' : ''; return sign + f.toFixed(2) + '%'; } function fmtNum(n) { if (n == null) return '-'; return 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(t, rank) { const fluCls = rateClass(t.fluRt); const periodCls = t.periodRt >= 0 ? 'text-purple-600' : 'text-blue-500'; const isSelected = t.code === selectedCode; return `
${rank} ${t.name}
${t.mainStock || '-'}
${fmtRate(t.fluRt)}
${fmtRate(t.periodRt)}
${t.stockCount}
${t.risingCount}▲ / ${t.fallCount}▼
`; } // ── 헤더 정렬 화살표 갱신 ──────────────────────────────────── function updateThemeColHeaders() { document.querySelectorAll('.theme-col-sort').forEach(el => { const arrow = el.querySelector('.sort-arrow'); if (!arrow) return; if (el.dataset.col === clientSortCol) { arrow.textContent = clientSortDesc ? '▼' : '▲'; 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'); } }); } // ── 클라이언트 정렬 적용 ───────────────────────────────────── function applyClientSort(themes) { if (!clientSortCol) return themes; return [...themes].sort((a, b) => { const av = a[clientSortCol], bv = b[clientSortCol]; if (typeof av === 'string') { const cmp = av.localeCompare(bv, 'ko'); return clientSortDesc ? cmp : -cmp; } const diff = bv - av; return clientSortDesc ? diff : -diff; }); } // ── 테마 목록 렌더 ─────────────────────────────────────────── function renderList(themes) { const q = searchEl.value.trim(); let filtered = q ? themes.filter(t => t.name.includes(q)) : themes; filtered = applyClientSort(filtered); countEl.textContent = `총 ${filtered.length}개 테마`; if (filtered.length === 0) { listEl.innerHTML = `
검색 결과가 없습니다.
`; return; } listEl.innerHTML = filtered.map((t, i) => makeRow(t, i + 1)).join(''); // 행 클릭 이벤트 listEl.querySelectorAll('.theme-row').forEach(row => { row.addEventListener('click', () => { const code = row.dataset.code; const name = row.dataset.name; if (code === selectedCode) return; selectedCode = code; selectedName = name; // 선택 상태 표시 갱신 listEl.querySelectorAll('.theme-row').forEach(r => { r.classList.toggle('bg-blue-50', r.dataset.code === code); r.classList.toggle('hover:bg-gray-50', r.dataset.code !== code); }); loadDetail(code, name); }); }); } // ── 테마 목록 조회 ─────────────────────────────────────────── async function loadThemes() { listEl.innerHTML = `
데이터를 불러오는 중...
`; try { const resp = await fetch(`/api/themes?date=${currentDate}&sort=${currentSort}`); if (!resp.ok) throw new Error('조회 실패'); allThemes = await resp.json(); renderList(allThemes); } catch (e) { listEl.innerHTML = `
데이터를 불러오지 못했습니다.
`; console.error('테마 목록 조회 실패:', e); } } // ── 구성종목 조회 ───────────────────────────────────────────── async function loadDetail(code, name) { emptyEl.classList.add('hidden'); contentEl.classList.add('hidden'); loadingEl.classList.remove('hidden'); try { const resp = await fetch(`/api/themes/${code}?date=${currentDate}`); if (!resp.ok) throw new Error('조회 실패'); const data = await resp.json(); nameEl.textContent = name; fluRtEl.textContent = fmtRate(data.fluRt); fluRtEl.className = 'font-semibold ' + rateClass(data.fluRt); periodRtEl.textContent = fmtRate(data.periodRt); const stocks = data.stocks || []; if (stocks.length === 0) { stockListEl.innerHTML = `
구성종목이 없습니다.
`; } else { stockListEl.innerHTML = stocks.map(s => { const cls = rateClass(s.fluRt); const bgCls = rateBg(s.fluRt); const sign = s.predPre >= 0 ? '+' : ''; return `

${s.name}

${s.code}

${fmtNum(s.curPrc)}원

${sign}${fmtRate(s.fluRt)}
`; }).join(''); } loadingEl.classList.add('hidden'); contentEl.classList.remove('hidden'); } catch (e) { loadingEl.classList.add('hidden'); emptyEl.classList.remove('hidden'); emptyEl.innerHTML = '

구성종목을 불러오지 못했습니다.

'; console.error('테마 구성종목 조회 실패:', e); } } // ── 날짜 탭 이벤트 ─────────────────────────────────────────── document.querySelectorAll('.date-tab').forEach(btn => { btn.addEventListener('click', () => { currentDate = btn.dataset.date; document.querySelectorAll('.date-tab').forEach(b => { b.className = b.dataset.date === currentDate ? 'date-tab px-3 py-1.5 bg-blue-500 text-white text-xs font-medium' : 'date-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200 text-xs font-medium'; }); loadThemes(); }); }); // ── 정렬 탭 이벤트 (서버 사이드) ───────────────────────────── document.querySelectorAll('.sort-tab').forEach(btn => { btn.addEventListener('click', () => { currentSort = btn.dataset.sort; // 헤더 클라이언트 정렬 초기화 (서버 순서 우선) clientSortCol = null; document.querySelectorAll('.sort-tab').forEach(b => { b.className = b.dataset.sort === currentSort ? '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'; }); updateThemeColHeaders(); loadThemes(); }); }); // ── 헤더 컬럼 클릭 정렬 ────────────────────────────────────── document.querySelectorAll('.theme-col-sort').forEach(el => { el.addEventListener('click', () => { const col = el.dataset.col; if (clientSortCol === col) { clientSortDesc = !clientSortDesc; } else { clientSortCol = col; clientSortDesc = true; } updateThemeColHeaders(); renderList(allThemes); }); }); // ── 검색 필터 이벤트 ───────────────────────────────────────── searchEl.addEventListener('input', () => renderList(allThemes)); // ── 초기 로드 ──────────────────────────────────────────────── loadThemes(); })();