프론트엔드 추가 및 자동매매 로직 개선:
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 1m42s

- Svelte 기반 프론트엔드 프로젝트 초기 설정 추가 (`vite`, `tailwindcss` 등 포함).
- "자동매매" 주요 상태 및 규칙 관리 페이지 구현.
- 1차/2차 손절 및 익절 조건 평가 로직 추가(`calcStopTargets`, `evalExitReason` 등).
- 포지션 상세 로그 및 WebSocket 기반 실시간 로그 스트림 추가.
- API 서비스 및 Frontend 간 Proxy 설정(Vite 서버).
- 세션 체크를 위한 `CheckSession` 핸들러 추가.
This commit is contained in:
hayato5246
2026-04-05 20:30:52 +09:00
parent f10a1ede3b
commit 00ffc6b54c
58 changed files with 6425 additions and 104 deletions

View File

@@ -351,27 +351,39 @@
// ─────────────────────────────────────────────
function renderSidebar() {
const list = loadList();
Array.from(sidebarEl.children).forEach(el => {
if (el.id !== 'watchlistEmpty') el.remove();
});
list.forEach(s => sidebarEl.appendChild(makeSidebarItem(s.code, s.name)));
// 테이블 생성
let tableEl = sidebarEl.querySelector('table');
if (tableEl) tableEl.remove();
if (list.length > 0) {
tableEl = document.createElement('table');
tableEl.className = 'w-full text-xs';
const tbody = document.createElement('tbody');
tbody.className = 'divide-y divide-gray-50';
list.forEach(s => tbody.appendChild(makeSidebarItem(s.code, s.name)));
tableEl.appendChild(tbody);
sidebarEl.insertBefore(tableEl, emptyEl);
}
updateEmptyStates();
}
function makeSidebarItem(code, name) {
const li = document.createElement('li');
li.id = `si-${code}`;
li.className = 'flex items-center justify-between px-3 py-2.5 hover:bg-gray-50 group';
li.innerHTML = `
<a href="/stock/${code}" class="flex-1 min-w-0">
<p class="text-xs font-medium text-gray-800 truncate">${name}</p>
<p class="text-xs text-gray-400 font-mono">${code}</p>
</a>
<button onclick="removeStock('${code}')" title="삭제"
class="ml-2 text-gray-300 hover:text-red-400 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-base leading-none">
×
</button>`;
return li;
const tr = document.createElement('tr');
tr.id = `si-${code}`;
tr.className = 'hover:bg-gray-50 group cursor-pointer';
tr.onclick = () => { window.location.href = `/stock/${code}`; };
tr.innerHTML = `
<td class="px-3 py-2">
<p class="font-medium text-gray-800 truncate">${name}</p>
<p class="text-gray-400 font-mono">${code}</p>
</td>
<td id="si-price-${code}" class="px-2 py-2 text-right font-mono text-gray-400">-</td>
<td id="si-rate-${code}" class="px-2 py-2 text-right text-gray-400">-</td>
<td id="si-cntr-${code}" class="px-2 py-2 text-right text-gray-400">-</td>
<td class="pr-2 py-2 text-center">
<button onclick="event.stopPropagation(); removeStock('${code}')" title="삭제"
class="text-gray-300 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-base leading-none">×</button>
</td>`;
return tr;
}
// ─────────────────────────────────────────────
@@ -437,17 +449,21 @@
const rateEl = document.getElementById(`wc-rate-${code}`);
const volEl = document.getElementById(`wc-vol-${code}`);
const cntrEl = document.getElementById(`wc-cntr-${code}`);
if (!priceEl) return;
const rate = data.changeRate ?? 0;
const colorCls = rateClass(rate);
const bgCls = rateBadgeClass(rate);
const sign = rate > 0 ? '+' : '';
priceEl.textContent = fmtNum(data.currentPrice) + '원';
priceEl.className = `text-xl font-bold mb-2 ${colorCls}`;
rateEl.textContent = sign + rate.toFixed(2) + '%';
rateEl.className = `text-xs px-2 py-0.5 rounded-full font-semibold ${bgCls}`;
// 패널 카드 업데이트
if (priceEl) {
priceEl.textContent = fmtNum(data.currentPrice) + '';
priceEl.className = `text-xl font-bold mb-2 ${colorCls}`;
}
if (rateEl) {
rateEl.textContent = sign + rate.toFixed(2) + '%';
rateEl.className = `text-xs px-2 py-0.5 rounded-full font-semibold ${bgCls}`;
}
if (volEl) volEl.textContent = fmtNum(data.volume);
if (cntrEl && data.cntrStr !== undefined) {
const cs = data.cntrStr;
@@ -455,6 +471,24 @@
cntrEl.className = `font-bold ${cs >= 100 ? 'text-orange-500' : 'text-blue-400'}`;
}
// 사이드바 행 업데이트
const siPrice = document.getElementById(`si-price-${code}`);
const siRate = document.getElementById(`si-rate-${code}`);
const siCntr = document.getElementById(`si-cntr-${code}`);
if (siPrice) {
siPrice.textContent = fmtNum(data.currentPrice);
siPrice.className = `px-2 py-2 text-right font-mono ${colorCls}`;
}
if (siRate) {
siRate.textContent = fmtRate(rate);
siRate.className = `px-2 py-2 text-right ${colorCls}`;
}
if (siCntr && data.cntrStr !== undefined) {
const cs = data.cntrStr;
siCntr.textContent = fmtCntr(cs);
siCntr.className = `px-2 py-2 text-right ${cs >= 100 ? 'text-orange-500 font-bold' : cs > 0 ? 'text-blue-400' : 'text-gray-400'}`;
}
// 체결강도 히스토리 기록 + 미니 차트 갱신
if (data.cntrStr != null && data.cntrStr !== 0) {
recordCntr(code, data.cntrStr);
@@ -536,7 +570,7 @@
// ─────────────────────────────────────────────
function updateEmptyStates() {
const hasItems = cachedList.length > 0;
emptyEl.classList.toggle('hidden', hasItems);
if (emptyEl) emptyEl.classList.toggle('hidden', hasItems);
panelEmpty?.classList.toggle('hidden', hasItems);
}