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

163
templates/layout/base.html Normal file
View File

@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- TradingView Lightweight Charts v4 (v5는 API 호환성 문제로 고정) -->
<script src="https://unpkg.com/lightweight-charts@4.2.1/dist/lightweight-charts.standalone.production.js"></script>
<link rel="stylesheet" href="/static/css/custom.css">
<style>
/* 상승/하락 색상 (한국 주식 관행) */
.price-up { color: #ef4444; } /* 빨강 */
.price-down { color: #3b82f6; } /* 파랑 */
.price-flat { color: #6b7280; } /* 회색 */
/* 사이드바 토글 애니메이션 */
#sidebarWrapper {
display: flex;
overflow: hidden;
transition: width 0.25s ease, opacity 0.25s ease, margin 0.25s ease;
width: 240px;
opacity: 1;
}
#sidebarWrapper.sidebar-hidden {
width: 0;
opacity: 0;
margin-right: 0 !important;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 네비게이션 -->
<nav class="bg-white shadow-sm sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14">
<!-- 사이드바 토글 버튼 -->
<button id="sidebarToggle" title="사이드바 열기/닫기"
class="mr-2 p-1.5 rounded-lg text-gray-500 hover:bg-gray-100 transition-colors shrink-0">
<!-- 사이드바 열림: 왼쪽 화살표(닫기) -->
<svg id="sidebarIconOpen" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/>
</svg>
<!-- 사이드바 닫힘: 오른쪽 화살표(열기) -->
<svg id="sidebarIconClose" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
</button>
<!-- 로고 -->
<a href="/" class="text-lg font-bold text-gray-800 hover:text-blue-600 shrink-0 mr-2">
📈 주식 시세
</a>
<!-- 네비 메뉴 -->
<nav class="flex items-center gap-0.5 shrink-0">
<a href="/" class="text-sm px-3 py-1.5 rounded-lg font-medium transition-colors
{{ if eq .ActiveMenu "시세" }}bg-blue-50 text-blue-600{{ else }}text-gray-500 hover:bg-gray-100 hover:text-gray-800{{ end }}">시세</a>
<a href="/theme" class="text-sm px-3 py-1.5 rounded-lg font-medium transition-colors
{{ if eq .ActiveMenu "테마" }}bg-blue-50 text-blue-600{{ else }}text-gray-500 hover:bg-gray-100 hover:text-gray-800{{ end }}">테마</a>
<a href="/kospi200" class="text-sm px-3 py-1.5 rounded-lg font-medium transition-colors
{{ if eq .ActiveMenu "코스피200" }}bg-blue-50 text-blue-600{{ else }}text-gray-500 hover:bg-gray-100 hover:text-gray-800{{ end }}">코스피200</a>
{{ if .LoggedIn }}
<a href="/asset" class="text-sm px-3 py-1.5 rounded-lg font-medium transition-colors
{{ if eq .ActiveMenu "자산" }}bg-blue-50 text-blue-600{{ else }}text-gray-500 hover:bg-gray-100 hover:text-gray-800{{ end }}">자산</a>
<a href="/autotrade" class="text-sm px-3 py-1.5 rounded-lg font-medium transition-colors
{{ if eq .ActiveMenu "자동매매" }}bg-blue-50 text-blue-600{{ else }}text-gray-500 hover:bg-gray-100 hover:text-gray-800{{ end }}">자동매매</a>
{{ end }}
</nav>
<!-- 검색창 -->
<div class="flex-1 max-w-md mx-4 relative">
<input
id="searchInput"
type="text"
placeholder="종목명 또는 코드 검색"
autocomplete="off"
class="w-full px-4 py-2 text-sm border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-400"
>
<!-- 자동완성 드롭다운 -->
<div id="searchDropdown" class="hidden absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 max-h-60 overflow-y-auto"></div>
</div>
<!-- 로그인/로그아웃 버튼 -->
{{ if .LoggedIn }}
<form method="POST" action="/logout" class="shrink-0">
<button type="submit"
class="text-xs px-3 py-1.5 rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-800 font-medium transition-colors">
로그아웃
</button>
</form>
{{ else }}
<a href="/login" class="shrink-0 text-xs px-3 py-1.5 rounded-lg text-blue-600 hover:bg-blue-50 font-medium transition-colors">
로그인
</a>
{{ end }}
</div>
</div>
</nav>
<!-- 지수 티커 바 -->
<div class="bg-gray-800 text-white text-xs">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-8 flex items-center gap-6 overflow-x-auto" id="indexTicker">
<span class="text-gray-400 shrink-0">로딩 중...</span>
</div>
</div>
<!-- 메인 콘텐츠 -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 flex gap-6 items-start">
<div id="sidebarWrapper">
{{ block "sidebar" . }}{{ end }}
</div>
<main class="flex-1 min-w-0">
{{ block "content" . }}{{ end }}
</main>
</div>
<!-- 공통 JavaScript -->
<script src="/static/js/websocket.js"></script>
<script src="/static/js/search.js"></script>
<script src="/static/js/indices.js"></script>
{{ block "scripts" . }}{{ end }}
<script>
(function () {
const wrapper = document.getElementById('sidebarWrapper');
const toggle = document.getElementById('sidebarToggle');
const iconOpen = document.getElementById('sidebarIconOpen');
const iconClose = document.getElementById('sidebarIconClose');
if (!wrapper || !toggle) return;
// 사이드바가 없는 페이지(sidebar 블록이 비어 있음)는 버튼 숨김
if (!wrapper.children.length) {
toggle.classList.add('hidden');
return;
}
const STORAGE_KEY = 'sidebar_visible';
function applyState(visible) {
if (visible) {
wrapper.classList.remove('sidebar-hidden');
iconOpen.classList.remove('hidden');
iconClose.classList.add('hidden');
} else {
wrapper.classList.add('sidebar-hidden');
iconOpen.classList.add('hidden');
iconClose.classList.remove('hidden');
}
localStorage.setItem(STORAGE_KEY, visible ? '1' : '0');
}
// 저장된 상태 복원 (기본값: 표시)
const saved = localStorage.getItem(STORAGE_KEY);
applyState(saved !== '0');
toggle.addEventListener('click', () => {
const isHidden = wrapper.classList.contains('sidebar-hidden');
applyState(isHidden); // 현재 숨겨져 있으면 보이게
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,98 @@
{{ template "base.html" . }}
{{ define "sidebar" }}{{ end }}
{{ define "content" }}
<div class="space-y-5">
<!-- 페이지 제목 -->
<h1 class="text-xl font-bold text-gray-800">자산 현황</h1>
<!-- 요약 카드 4개 -->
<div id="summaryCards" class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 animate-pulse">
<p class="text-xs text-gray-400 mb-1">로딩 중...</p>
<p class="text-lg font-bold text-gray-300">-</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 animate-pulse">
<p class="text-xs text-gray-400 mb-1">로딩 중...</p>
<p class="text-lg font-bold text-gray-300">-</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 animate-pulse">
<p class="text-xs text-gray-400 mb-1">로딩 중...</p>
<p class="text-lg font-bold text-gray-300">-</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 animate-pulse">
<p class="text-xs text-gray-400 mb-1">로딩 중...</p>
<p class="text-lg font-bold text-gray-300">-</p>
</div>
</div>
<!-- 예수금 카드 -->
<div id="cashCard" class="bg-white rounded-xl shadow-sm border border-gray-100 p-5">
<h2 class="text-sm font-semibold text-gray-700 mb-3">예수금</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-xs text-gray-400 mb-1">D+2 추정예수금</p>
<p id="cashEntr" class="text-base font-bold text-gray-800">-</p>
</div>
<div>
<p class="text-xs text-gray-400 mb-1">주문가능현금</p>
<p id="cashOrdAlowa" class="text-base font-bold text-gray-800">-</p>
</div>
</div>
</div>
<!-- 보유 종목 테이블 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100">
<h2 class="text-sm font-semibold text-gray-700">보유 종목</h2>
</div>
<!-- 테이블 헤더 -->
<div class="grid grid-cols-[1fr_80px_90px_90px_100px_80px] text-xs font-semibold text-gray-500 bg-gray-50 border-b border-gray-100 px-5 py-2.5 gap-2">
<span>종목명</span>
<span class="text-right">보유수량</span>
<span class="text-right">평균단가</span>
<span class="text-right">현재가</span>
<span class="text-right">평가손익</span>
<span class="text-right">수익률</span>
</div>
<!-- 종목 목록 -->
<div id="holdingsTable">
<div class="px-5 py-10 text-center text-sm text-gray-400 animate-pulse">데이터를 불러오는 중...</div>
</div>
</div>
<!-- 미체결/체결내역 탭 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<!-- 탭 버튼 -->
<div class="flex border-b border-gray-100">
<button id="assetPendingTab"
onclick="showAssetTab('pending')"
class="px-5 py-3 text-sm font-medium border-b-2 border-blue-500 text-blue-600">
미체결
</button>
<button id="assetHistoryTab"
onclick="showAssetTab('history')"
class="px-5 py-3 text-sm font-medium text-gray-500 hover:text-gray-700">
체결내역
</button>
</div>
<!-- 미체결 패널 -->
<div id="assetPendingPanel" class="p-5">
<div class="text-sm text-gray-400 text-center py-6 animate-pulse">조회 중...</div>
</div>
<!-- 체결내역 패널 -->
<div id="assetHistoryPanel" class="p-5 hidden">
<div class="text-sm text-gray-400 text-center py-6">탭을 클릭하면 조회됩니다.</div>
</div>
</div>
</div>
{{ end }}
{{ define "scripts" }}
<script src="/static/js/asset.js"></script>
{{ end }}

View File

@@ -0,0 +1,266 @@
{{ template "base.html" . }}
{{ define "sidebar" }}{{ end }}
{{ define "content" }}
<div class="space-y-5">
<!-- 페이지 제목 + 상태바 -->
<div class="flex items-center justify-between flex-wrap gap-3">
<h1 class="text-xl font-bold text-gray-800">자동매매</h1>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<span id="statusDot" class="w-3 h-3 rounded-full bg-gray-300"></span>
<span id="statusText" class="text-sm font-medium text-gray-600">중지</span>
</div>
<button onclick="startEngine()" id="btnStart"
class="px-4 py-2 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
시작
</button>
<button onclick="stopEngine()" id="btnStop"
class="px-4 py-2 text-sm font-medium bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors">
중지
</button>
<button onclick="emergencyStop()"
class="px-4 py-2 text-sm font-medium bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
⚠ 긴급청산
</button>
</div>
</div>
<!-- 요약 카드 3개 -->
<div class="grid grid-cols-3 gap-4">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<p class="text-xs text-gray-500 mb-1">진행중 포지션</p>
<p id="statActivePos" class="text-2xl font-bold text-gray-800">0</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<p class="text-xs text-gray-500 mb-1">오늘 매매 횟수</p>
<p id="statTradeCount" class="text-2xl font-bold text-gray-800">0</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<p class="text-xs text-gray-500 mb-1">오늘 손익</p>
<p id="statTotalPL" class="text-2xl font-bold text-gray-800">0원</p>
</div>
</div>
<!-- 감시 소스 설정 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold text-gray-800">감시 소스 설정</h2>
<span class="text-xs text-gray-400">매수 신호를 가져올 종목 범위를 선택하세요</span>
</div>
<div class="flex flex-col lg:flex-row gap-4">
<!-- 체결강도 자동감지 토글 -->
<div class="flex items-center gap-3 p-3 rounded-lg border border-gray-100 bg-gray-50 min-w-[200px]">
<div class="flex-1">
<p class="text-sm font-medium text-gray-800">체결강도 자동감지</p>
<p class="text-xs text-gray-400">거래량 상위 20종목 실시간 분석</p>
</div>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="wsScanner" checked onchange="saveWatchSource()"
class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 peer-checked:bg-blue-600 rounded-full peer
peer-focus:ring-2 peer-focus:ring-blue-300 transition-colors relative
after:content-[''] after:absolute after:top-[2px] after:left-[2px]
after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all
peer-checked:after:translate-x-5"></div>
</label>
</div>
<!-- 테마 선택 -->
<div class="flex-1 p-3 rounded-lg border border-gray-100 bg-gray-50">
<p class="text-sm font-medium text-gray-800 mb-2">테마 감시</p>
<!-- 테마 드롭다운 + 추가 버튼 -->
<div class="flex gap-2 mb-2">
<select id="themeSelect"
class="flex-1 px-3 py-1.5 text-xs border border-gray-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-400">
<option value="">테마를 선택하세요...</option>
</select>
<button onclick="addSelectedTheme()"
class="px-3 py-1.5 text-xs font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors whitespace-nowrap">
+ 추가
</button>
</div>
<!-- 선택된 테마 태그 목록 -->
<div id="selectedThemes" class="flex flex-wrap gap-1.5 min-h-[28px]">
<span class="text-xs text-gray-400" id="noThemeMsg">선택된 테마 없음</span>
</div>
</div>
</div>
</div>
<!-- 규칙 + 포지션 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 매매 규칙 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold text-gray-800">매매 규칙</h2>
<button onclick="showAddRuleModal()"
class="px-3 py-1.5 text-xs font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
+ 규칙 추가
</button>
</div>
<div id="rulesList" class="space-y-3">
<p class="text-xs text-gray-400 text-center py-4">규칙이 없습니다.</p>
</div>
</div>
<!-- 현재 포지션 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold text-gray-800">현재 포지션</h2>
<span class="text-xs text-gray-400">5초 자동 갱신</span>
</div>
<div id="positionsList">
<p class="text-xs text-gray-400 text-center py-4">보유 포지션 없음</p>
</div>
</div>
</div>
<!-- 매매 로그 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between mb-3 flex-wrap gap-2">
<div class="flex items-center gap-2">
<h2 class="text-sm font-semibold text-gray-800">매매 로그</h2>
<span id="wsStatus" class="text-xs text-gray-400">○ 연결중...</span>
</div>
<div class="flex gap-1 text-xs">
<button onclick="setLogFilter('all')" id="filterAll"
class="px-3 py-1 rounded-full bg-gray-800 text-white font-medium">전체</button>
<button onclick="setLogFilter('action')" id="filterAction"
class="px-3 py-1 rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200">매매만</button>
</div>
<label class="flex items-center gap-1 text-xs text-gray-400 cursor-pointer">
<input type="checkbox" id="autoScrollChk" checked class="w-3 h-3 accent-blue-500">
자동스크롤
</label>
<span id="logUpdateTime" class="text-xs text-gray-400"></span>
</div>
<div id="logsWrapper" class="overflow-y-auto rounded border border-gray-100" style="max-height:480px;">
<table class="w-full text-xs">
<thead class="sticky top-0 bg-white">
<tr class="text-left text-gray-400 border-b border-gray-100">
<th class="pb-2 pt-2 px-1 font-medium w-36">시각</th>
<th class="pb-2 pt-2 px-1 font-medium w-16">레벨</th>
<th class="pb-2 pt-2 px-1 font-medium w-20">종목</th>
<th class="pb-2 pt-2 px-1 font-medium">내용</th>
</tr>
</thead>
<tbody id="logsList">
<tr><td colspan="4" class="py-4 text-center text-gray-400">로그가 없습니다.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 규칙 추가/수정 모달 -->
<div id="ruleModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex items-center justify-between mb-5">
<h3 id="modalTitle" class="text-base font-semibold text-gray-800">규칙 추가</h3>
<button onclick="hideModal()" class="text-gray-400 hover:text-gray-600 text-lg"></button>
</div>
<div class="space-y-4">
<input type="hidden" id="ruleId">
<!-- 규칙명 -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">규칙명</label>
<input id="fName" type="text" placeholder="예: 강한매수 전략"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
<!-- 진입 조건 -->
<div class="border-t pt-4">
<p class="text-xs font-semibold text-gray-600 mb-3">진입 조건</p>
<div class="space-y-3">
<div>
<label class="flex items-center justify-between text-xs text-gray-700 mb-1">
<span>최소 상승점수 (RiseScore)</span>
<span id="riseScoreVal" class="font-semibold text-blue-600">60</span>
</label>
<input id="fRiseScore" type="range" min="0" max="100" value="60"
oninput="document.getElementById('riseScoreVal').textContent=this.value"
class="w-full accent-blue-600">
</div>
<div>
<label class="block text-xs text-gray-700 mb-1">최소 체결강도</label>
<input id="fCntrStr" type="number" value="110" step="1" min="0"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
<div class="flex items-center gap-2">
<input id="fRequireBullish" type="checkbox" class="w-4 h-4 accent-blue-600">
<label class="text-xs text-gray-700">AI 호재 신호 필요</label>
</div>
</div>
</div>
<!-- 주문 설정 -->
<div class="border-t pt-4">
<p class="text-xs font-semibold text-gray-600 mb-3">주문 설정</p>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-700 mb-1">1회 주문금액 (원)</label>
<input id="fOrderAmount" type="number" value="500000" step="10000" min="10000"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
<div>
<label class="block text-xs text-gray-700 mb-1">최대 보유 종목 수</label>
<input id="fMaxPositions" type="number" value="3" min="1" max="20"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
</div>
</div>
<!-- 청산 조건 -->
<div class="border-t pt-4">
<p class="text-xs font-semibold text-gray-600 mb-3">청산 조건</p>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-700 mb-1">손절 (%)</label>
<input id="fStopLoss" type="number" value="-3" step="0.5"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
<div>
<label class="block text-xs text-gray-700 mb-1">익절 (%)</label>
<input id="fTakeProfit" type="number" value="5" step="0.5"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
<div>
<label class="block text-xs text-gray-700 mb-1">최대 보유 시간 (분, 0=무제한)</label>
<input id="fMaxHold" type="number" value="60" min="0"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
<div class="flex items-end pb-2">
<div class="flex items-center gap-2">
<input id="fExitBeforeClose" type="checkbox" checked class="w-4 h-4 accent-blue-600">
<label class="text-xs text-gray-700">장마감 전 청산 (15:20)</label>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-2">
<button type="button" onclick="submitRule()"
class="flex-1 py-2.5 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
저장
</button>
<button type="button" onclick="hideModal()"
class="flex-1 py-2.5 text-sm font-medium bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors">
취소
</button>
</div>
</div>
</div>
</div>
</div>
{{ end }}
{{ define "scripts" }}
<script src="/static/js/autotrade.js"></script>
{{ end }}

338
templates/pages/index.html Normal file
View File

@@ -0,0 +1,338 @@
{{ template "base.html" . }}
{{ define "sidebar" }}
<aside class="w-60 shrink-0 sticky top-20 self-start">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-3 bg-gray-50 border-b border-gray-100">
<h2 class="font-bold text-gray-800 text-sm">관심종목</h2>
</div>
<!-- 종목 추가 입력 -->
<div class="p-3 border-b border-gray-100">
<div class="flex gap-2">
<input id="watchlistInput" type="text" placeholder="종목코드 (예: 005930)"
maxlength="6"
class="flex-1 text-xs px-2 py-1.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-400 font-mono">
<button id="watchlistAddBtn"
class="text-xs px-2.5 py-1.5 bg-blue-500 text-white rounded-lg hover:bg-blue-600 font-medium shrink-0">
추가
</button>
</div>
<p id="watchlistMsg" class="text-xs text-red-500 mt-1 hidden"></p>
</div>
<!-- 관심종목 목록 -->
<ul id="watchlistSidebar" class="divide-y divide-gray-50 max-h-[60vh] overflow-y-auto">
<li class="px-4 py-8 text-center text-xs text-gray-400" id="watchlistEmpty">
관심종목을 추가해주세요
</li>
</ul>
</div>
</aside>
{{ end }}
{{ define "content" }}
<div class="space-y-8">
<!-- 체결강도 상승 감지 -->
<section>
<h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<span class="text-orange-500"></span> 체결강도 상승 감지
<span class="text-xs font-normal text-gray-400 ml-1">(거래량 상위 20 | 10초 갱신)</span>
<button id="signalGuideBtn"
class="w-5 h-5 rounded-full bg-gray-200 text-gray-500 text-xs font-bold hover:bg-orange-100 hover:text-orange-600 transition-colors flex items-center justify-center"
title="판단 기준 보기">?</button>
<button id="scannerToggleBtn"
class="text-xs px-2.5 py-1 rounded-full font-semibold border transition-colors bg-green-100 text-green-700 border-green-300"
title="스캐너 ON/OFF">● ON</button>
<span id="signalUpdatedAt" class="text-xs font-normal text-gray-400 ml-auto"></span>
</h2>
<!-- 시그널 판단 기준 모달 -->
<div id="signalGuideModal"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 hidden"
role="dialog" aria-modal="true">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 pt-5 pb-3 border-b border-gray-100 sticky top-0 bg-white">
<h3 class="text-base font-bold text-gray-800">⚡ 시그널 판단 기준</h3>
<button id="signalGuideClose" class="text-gray-400 hover:text-gray-600 text-xl font-bold leading-none"></button>
</div>
<div class="px-6 py-5 space-y-6 text-sm text-gray-700">
<!-- 스코어 설명 -->
<div>
<p class="font-semibold text-gray-800 mb-2">📊 상승 확률 스코어 (0~100점)</p>
<p class="text-xs text-gray-400 mb-3">4가지 복합 요소를 동시에 만족해야 진짜 상승 확률이 높습니다.</p>
<table class="w-full text-xs border-collapse">
<thead>
<tr class="bg-gray-50 text-gray-500">
<th class="text-left px-3 py-2 border border-gray-100 font-semibold">요소</th>
<th class="text-center px-3 py-2 border border-gray-100 font-semibold w-16">배점</th>
<th class="text-left px-3 py-2 border border-gray-100 font-semibold">핵심 로직</th>
</tr>
</thead>
<tbody>
<tr>
<td class="px-3 py-2 border border-gray-100 font-medium">A. 체결강도 레벨</td>
<td class="px-3 py-2 border border-gray-100 text-center text-gray-500">0~30</td>
<td class="px-3 py-2 border border-gray-100 text-gray-500">150+: 30점 · 130+: 22점 · 110+: 14점</td>
</tr>
<tr class="bg-gray-50">
<td class="px-3 py-2 border border-gray-100 font-medium">B. 연속 상승 횟수</td>
<td class="px-3 py-2 border border-gray-100 text-center text-gray-500">0~25</td>
<td class="px-3 py-2 border border-gray-100 text-gray-500">5회+: 25점 · 3회+: 14점 · 1회: 3점</td>
</tr>
<tr>
<td class="px-3 py-2 border border-gray-100 font-medium">C. 가격 위치/캔들</td>
<td class="px-3 py-2 border border-gray-100 text-center text-gray-500">0~20</td>
<td class="px-3 py-2 border border-gray-100 text-gray-500">윗꼬리 10% 미만: +10점 · 60% 이상: 8점</td>
</tr>
<tr class="bg-gray-50">
<td class="px-3 py-2 border border-gray-100 font-medium">D. 거래량 건전성</td>
<td class="px-3 py-2 border border-gray-100 text-center text-gray-500">0~15</td>
<td class="px-3 py-2 border border-gray-100 text-gray-500">2~5배 증가: +15점 · 10배 이상: 5점</td>
</tr>
<tr>
<td class="px-3 py-2 border border-gray-100 font-medium">E. 매도잔량 소화</td>
<td class="px-3 py-2 border border-gray-100 text-center text-gray-500">0~10</td>
<td class="px-3 py-2 border border-gray-100 text-gray-500">매수잔량 압도적: +10점 · 매도 우세: 3점</td>
</tr>
</tbody>
</table>
</div>
<!-- 신호 유형 설명 -->
<div>
<p class="font-semibold text-gray-800 mb-2">🏷️ 신호 유형 분류</p>
<div class="space-y-2">
<div class="flex items-start gap-3 p-2.5 rounded-lg bg-red-50">
<span class="bg-red-600 text-white text-xs px-2 py-0.5 rounded font-bold shrink-0 mt-0.5">강한매수</span>
<span class="text-gray-600">체결강도 130+ · 3회 이상 연속 상승 · 윗꼬리 없음 · 매도잔량 소화 중 → 실매수 강하고 던지는 물량도 받아내는 패턴</span>
</div>
<div class="flex items-start gap-3 p-2.5 rounded-lg bg-orange-50">
<span class="bg-orange-400 text-white text-xs px-2 py-0.5 rounded font-bold shrink-0 mt-0.5">매수우세</span>
<span class="text-gray-600">기본 상승 패턴. 강한매수 조건 미충족이나 전반적으로 매수세가 우위</span>
</div>
<div class="flex items-start gap-3 p-2.5 rounded-lg bg-yellow-50">
<span class="bg-yellow-100 text-yellow-700 border border-yellow-300 text-xs px-2 py-0.5 rounded font-bold shrink-0 mt-0.5">물량소화</span>
<span class="text-gray-600">체결강도는 높은데 가격이 제자리 + 긴 윗꼬리 → 위에서 파는 물량을 받아내기만 하는 중. 돌파 여부 확인 필요</span>
</div>
<div class="flex items-start gap-3 p-2.5 rounded-lg bg-gray-100">
<span class="bg-gray-800 text-white text-xs px-2 py-0.5 rounded font-bold shrink-0 mt-0.5">추격위험</span>
<span class="text-gray-600">체결강도 170+ · 거래량 7배 이상 · 긴 윗꼬리 동시 출현 → 단타 추격매수 몰림 후 고점 물량털기 가능성 경계</span>
</div>
<div class="flex items-start gap-3 p-2.5 rounded-lg bg-gray-50">
<span class="bg-gray-100 text-gray-500 border border-gray-300 text-xs px-2 py-0.5 rounded font-bold shrink-0 mt-0.5">약한상승</span>
<span class="text-gray-600">거래량 증가 없음 · 체결강도 120 미만 → 얇은 호가에서 뜬 상승, 소량 매도에도 쉽게 꺾일 수 있음</span>
</div>
</div>
</div>
<!-- 지표별 상세 설명 -->
<div>
<p class="font-semibold text-gray-800 mb-3">🔍 카드 지표 상세 설명</p>
<div class="space-y-3">
<!-- 체결강도 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">체결강도</span>
<span class="text-xs text-gray-400">· 매수/매도 힘의 비율</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600 space-y-1">
<p>10초마다 직전 체결강도와 비교해 <span class="font-semibold text-orange-500">연속으로 상승하는 종목</span>만 표시합니다.</p>
<p class="text-gray-400">100 = 매수·매도 균형 / 100 초과 = 매수 우위 / 100 미만 = 매도 우위</p>
<div class="flex flex-wrap gap-1.5 mt-1">
<span class="bg-red-50 text-red-500 px-2 py-0.5 rounded">150+ 강한 매수세</span>
<span class="bg-orange-50 text-orange-500 px-2 py-0.5 rounded">130~150 매수 우세</span>
<span class="bg-yellow-50 text-yellow-600 px-2 py-0.5 rounded">100~130 매수 경향</span>
<span class="bg-blue-50 text-blue-400 px-2 py-0.5 rounded">100 미만 매도 경향</span>
</div>
</div>
</div>
<!-- 연속 상승 뱃지 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">연속 상승 뱃지</span>
<span class="text-xs text-gray-400">· 10초 단위 체결강도 상승 횟수</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600 space-y-1.5">
<p>직전 측정값 대비 체결강도가 <span class="font-semibold">끊기지 않고 몇 번 연속으로 올랐는지</span>를 나타냅니다.</p>
<div class="flex flex-wrap gap-1.5">
<span class="bg-red-500 text-white px-2 py-0.5 rounded font-bold">🔥N연속</span>
<span class="text-gray-500 self-center">4회 이상 — 강한 지속 매수세</span>
</div>
<div class="flex flex-wrap gap-1.5">
<span class="bg-orange-400 text-white px-2 py-0.5 rounded font-bold">▲N연속</span>
<span class="text-gray-500 self-center">2~3회 — 매수세 형성 중</span>
</div>
<div class="flex flex-wrap gap-1.5">
<span class="bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded font-bold">↑상승</span>
<span class="text-gray-500 self-center">1회 — 초기 매수 감지</span>
</div>
</div>
</div>
<!-- 거래량 증가율 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">거래량 증가</span>
<span class="text-xs text-gray-400">· 직전 평균 대비 현재 구간 배수</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600 space-y-1">
<p>직전 6회(1분) 구간 평균 거래량 대비 <span class="font-semibold">현재 10초 구간에서 얼마나 폭발적으로 거래됐는지</span>를 나타냅니다.</p>
<div class="flex flex-wrap gap-1.5 mt-1">
<span class="bg-green-50 text-green-600 font-semibold px-2 py-0.5 rounded">2~5배 최적</span>
<span class="bg-orange-50 text-orange-400 px-2 py-0.5 rounded">5~10배 과열 초입</span>
<span class="bg-gray-100 text-gray-400 px-2 py-0.5 rounded line-through">10배+ ⚠과열</span>
</div>
<p class="text-gray-400 mt-1">※ 10배 이상은 고점 물량털기 가능성이 높아 스코어가 감점됩니다.</p>
</div>
</div>
<!-- 매도/매수 잔량비 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">매도/매수 잔량</span>
<span class="text-xs text-gray-400">· 호가창 전체 잔량 비율</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600 space-y-1">
<p>10단계 매도호가 총잔량 ÷ 10단계 매수호가 총잔량입니다. <span class="font-semibold">1보다 작을수록 매수 우위</span>, 클수록 위에 팔 물량이 많다는 의미입니다.</p>
<div class="flex flex-wrap gap-1.5 mt-1">
<span class="bg-green-50 text-green-600 font-semibold px-2 py-0.5 rounded">0.7 미만 — 매수 강세</span>
<span class="bg-green-50 text-green-500 px-2 py-0.5 rounded">0.7~1.0 — 매수 우세</span>
<span class="bg-gray-50 text-gray-500 px-2 py-0.5 rounded">1.0~1.5 — 균형</span>
<span class="bg-blue-50 text-blue-500 px-2 py-0.5 rounded">1.5+ — 매도 우세</span>
</div>
</div>
</div>
<!-- 가격 위치 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">가격 위치</span>
<span class="text-xs text-gray-400">· 장중 저가~고가 내 현재가 위치 %</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600 space-y-1">
<p>오늘 장중 저가를 0%, 고가를 100%로 봤을 때 <span class="font-semibold">현재가가 어디에 있는지</span>를 나타냅니다.</p>
<div class="flex flex-wrap gap-1.5 mt-1">
<span class="bg-red-50 text-red-500 font-semibold px-2 py-0.5 rounded">80%+ 고가권 · 강한 매수</span>
<span class="bg-orange-50 text-orange-400 px-2 py-0.5 rounded">60~80% 상단부</span>
<span class="bg-gray-50 text-gray-500 px-2 py-0.5 rounded">30~60% 중간대</span>
<span class="bg-blue-50 text-blue-400 px-2 py-0.5 rounded">30% 미만 저가권</span>
</div>
<p class="text-gray-400 mt-1">※ 체결강도가 강한데 가격 위치가 낮으면 저점 반등 초기 신호일 수 있습니다.</p>
</div>
</div>
<!-- AI 감성 분석 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">AI 감성 분석</span>
<span class="text-xs text-gray-400">· 최신 뉴스 기반 호재/악재 판단</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600 space-y-1.5">
<p>상승 확률 50점 이상 · 2회 이상 연속 상승 · 체결강도 100 이상 종목에 한해 AI가 최신 뉴스를 분석합니다.</p>
<div class="flex flex-wrap gap-1.5">
<span class="bg-green-100 text-green-700 border border-green-200 px-2 py-0.5 rounded font-semibold">호재</span>
<span class="text-gray-500 self-center">긍정적 뉴스·공시 감지</span>
</div>
<div class="flex flex-wrap gap-1.5">
<span class="bg-red-100 text-red-600 border border-red-200 px-2 py-0.5 rounded font-semibold">악재</span>
<span class="text-gray-500 self-center">부정적 뉴스·공시 감지</span>
</div>
<div class="flex flex-wrap gap-1.5">
<span class="bg-gray-100 text-gray-500 px-2 py-0.5 rounded font-semibold">중립</span>
<span class="text-gray-500 self-center">특이 뉴스 없음</span>
</div>
<p class="text-gray-400">※ 뱃지에 마우스를 올리면 AI가 판단한 근거 한 줄이 표시됩니다.</p>
</div>
</div>
<!-- AI 목표가 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">AI 목표가</span>
<span class="text-xs text-gray-400">· 단기 기술적 목표가 추론</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600 space-y-1">
<p>현재가·고가·저가·체결강도·연속상승 횟수·AI 감성을 종합해 <span class="font-semibold text-purple-600">단기 목표가</span>를 추론합니다. 투자 조언이 아닌 참고용 수치입니다.</p>
<p class="text-gray-400">※ 뱃지에 마우스를 올리면 목표가 추론 근거가 표시됩니다.</p>
</div>
</div>
<!-- 익일 추세 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">익일 추세</span>
<span class="text-xs text-gray-400">· 다음 거래일 방향성 예측</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600 space-y-1.5">
<p>당일 기술 지표와 AI 감성 분석을 결합해 다음 거래일 방향성을 예측합니다. 신뢰도(높음/보통/낮음)를 함께 표시합니다.</p>
<div class="flex flex-wrap gap-1.5">
<span class="bg-red-50 text-red-500 border border-red-200 px-2 py-0.5 rounded font-bold">▲ 상승</span>
<span class="bg-blue-50 text-blue-500 border border-blue-200 px-2 py-0.5 rounded font-bold">▼ 하락</span>
<span class="bg-gray-50 text-gray-500 border border-gray-200 px-2 py-0.5 rounded font-bold">─ 횡보</span>
</div>
<p class="text-gray-400">※ AI 예측은 참고용이며, 실제 시장 상황에 따라 달라질 수 있습니다.</p>
</div>
</div>
<!-- 체결강도 미니차트 -->
<div class="rounded-lg border border-gray-100 overflow-hidden">
<div class="px-3 py-2 bg-gray-50 flex items-center gap-2">
<span class="font-semibold text-gray-800 text-xs">체결강도 차트</span>
<span class="text-xs text-gray-400">· 카드 하단 주황색 라인</span>
</div>
<div class="px-3 py-2.5 text-xs text-gray-600">
<p>시그널 감지 이후 <span class="font-semibold text-orange-500">10초마다 수집된 체결강도 흐름</span>을 라인차트로 표시합니다. 우상향이면 매수세가 지속되는 중입니다. 최대 60포인트(10분) 유지됩니다.</p>
</div>
</div>
</div>
</div>
<!-- 한 줄 기준 -->
<div class="bg-orange-50 rounded-xl p-4 border border-orange-100">
<p class="font-semibold text-orange-700 mb-1">💡 진짜 상승 한 줄 기준</p>
<p class="text-orange-600">가격이 오르면서 · 거래량이 받쳐주고 · 체결강도가 유지되고 · 위 매도물량이 실제로 소화되는 흐름</p>
</div>
<!-- 면책 안내 -->
<div class="bg-gray-50 rounded-xl p-4 border border-gray-100">
<p class="font-semibold text-gray-600 mb-1">⚠️ 투자 유의사항</p>
<p class="text-gray-500 text-xs leading-relaxed">이 서비스의 모든 지표와 AI 분석은 <span class="font-semibold">참고용 정보</span>이며 투자 권유나 매매 신호가 아닙니다. 투자 결정은 본인의 판단과 책임 하에 이루어져야 하며, 모든 투자 손실에 대한 책임은 투자자 본인에게 있습니다.</p>
</div>
</div>
</div>
</div>
<div id="signalGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<p class="col-span-3 text-gray-400 text-center py-6 text-sm" id="signalEmpty">
08:00 이후 거래량 상위 종목에서 체결강도 100 이상 + 상승 종목을 표시합니다.
</p>
</div>
</section>
<!-- 관심종목 실시간 패널 -->
<section>
<h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<span class="text-yellow-500"></span> 관심종목 실시간
<span class="text-xs font-normal text-gray-400 ml-1" id="wsStatus">연결 중...</span>
</h2>
<div id="watchlistPanel" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<div id="watchlistPanelEmpty" class="col-span-3 text-center py-12 text-gray-400 text-sm bg-white rounded-xl border border-dashed border-gray-200">
좌측 메뉴에서 관심종목을 추가하면 실시간 시세가 표시됩니다.
</div>
</div>
</section>
</div>
{{ end }}
{{ define "scripts" }}
<script src="/static/js/signal.js"></script>
<script src="/static/js/watchlist.js"></script>
{{ end }}

View File

@@ -0,0 +1,61 @@
{{ template "base.html" . }}
{{ define "sidebar" }}{{ end }}
{{ define "content" }}
<div class="space-y-4">
<!-- 헤더 + 필터 바 -->
<div class="flex flex-wrap items-center gap-3">
<h1 class="text-xl font-bold text-gray-800 flex items-center gap-2">
<span class="text-blue-600">🔵</span> 코스피200
<span id="lastUpdated" class="text-xs font-normal text-gray-400"></span>
</h1>
<!-- 정렬 -->
<div class="flex rounded-lg border border-gray-200 overflow-hidden text-xs font-medium">
<button data-sort="fluRt" class="sort-tab px-3 py-1.5 bg-blue-500 text-white">등락률</button>
<button data-sort="volume" class="sort-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200">거래량</button>
<button data-sort="curPrc" class="sort-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200">현재가</button>
</div>
<!-- 상승/하락/전체 필터 -->
<div class="flex rounded-lg border border-gray-200 overflow-hidden text-xs font-medium">
<button data-dir="all" class="dir-tab px-3 py-1.5 bg-blue-500 text-white">전체</button>
<button data-dir="up" class="dir-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200">상승</button>
<button data-dir="down" class="dir-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200">하락</button>
</div>
<!-- 종목 검색 -->
<input id="k200Search" type="text" placeholder="종목명 검색..."
class="text-xs px-3 py-1.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-400 w-36">
<span id="k200Count" class="text-xs text-gray-400 ml-auto"></span>
</div>
<!-- 종목 테이블 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<!-- 테이블 헤더 -->
<div class="grid grid-cols-[2.5rem_1fr_1fr_90px_90px_100px_90px_90px_90px] text-xs font-semibold text-gray-500 bg-gray-50 border-b border-gray-100 px-4 py-2.5 gap-2">
<span class="text-center">#</span>
<span>종목명</span>
<span data-col="curPrc" class="col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">현재가 <span class="sort-arrow"></span></span>
<span data-col="predPre" class="col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">전일대비 <span class="sort-arrow"></span></span>
<span data-col="fluRt" class="col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">등락률 <span class="sort-arrow text-blue-400"></span></span>
<span data-col="volume" class="col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">거래량 <span class="sort-arrow"></span></span>
<span data-col="open" class="col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">시가 <span class="sort-arrow"></span></span>
<span data-col="high" class="col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">고가 <span class="sort-arrow"></span></span>
<span data-col="low" class="col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">저가 <span class="sort-arrow"></span></span>
</div>
<!-- 종목 목록 -->
<div id="k200List" class="divide-y divide-gray-50">
<div class="px-4 py-12 text-center text-sm text-gray-400 animate-pulse">데이터를 불러오는 중...</div>
</div>
</div>
</div>
{{ end }}
{{ define "scripts" }}
<script src="/static/js/kospi200.js"></script>
{{ end }}

View File

@@ -0,0 +1,59 @@
{{define "login.html"}}
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>로그인 - 주식 시세</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center">
<div class="bg-white rounded-2xl shadow-lg w-full max-w-sm p-8">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-800">📈 주식 시세</h1>
<p class="text-sm text-gray-400 mt-1">로그인이 필요한 서비스입니다.</p>
</div>
{{ if .Error }}
<div class="mb-4 px-4 py-3 bg-red-50 border border-red-200 rounded-lg">
<p class="text-sm text-red-600">{{ .Error }}</p>
</div>
{{ end }}
<form method="POST" action="/login" class="space-y-4">
<input type="hidden" name="next" value="{{ .Next }}">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">아이디</label>
<input
type="text"
name="id"
placeholder="아이디를 입력하세요"
autocomplete="username"
required
class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">비밀번호</label>
<input
type="password"
name="password"
placeholder="비밀번호를 입력하세요"
autocomplete="current-password"
required
class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
>
</div>
<button
type="submit"
class="w-full py-2.5 px-4 bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold rounded-lg transition-colors mt-2">
로그인
</button>
</form>
</div>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,407 @@
{{ template "base.html" . }}
{{ define "content" }}
<div class="space-y-4">
<!-- 현재가 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-5 border border-gray-100">
<!-- 종목명 + 장운영 상태 -->
<div class="flex items-center gap-2 mb-3">
<span class="text-xs text-gray-400 font-mono">{{ .Stock.Code }}</span>
<h1 class="text-xl font-bold text-gray-900">{{ .Stock.Name }}</h1>
<span id="marketStatusBadge" class="px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">장 중</span>
<button id="watchlistToggleBtn" onclick="toggleWatchlist()"
class="ml-auto flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium border transition-colors">
</button>
</div>
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<!-- 현재가 & 등락 -->
<div>
<p id="currentPrice" class="text-4xl font-bold text-gray-900" data-raw="{{ .Stock.CurrentPrice }}">
{{ formatPrice .Stock.CurrentPrice }}원
</p>
<p id="changeInfo" class="text-lg mt-1 {{ priceClass .Stock.ChangeRate }}">
{{ if gt .Stock.ChangePrice 0 }}+{{ end }}{{ formatPrice .Stock.ChangePrice }}원
({{ formatRate .Stock.ChangeRate }})
</p>
<p id="updatedAt" class="text-xs text-gray-400 mt-1">장 중</p>
</div>
<!-- 최우선 호가 -->
<div class="flex gap-4 text-sm">
<div class="text-center">
<p class="text-xs text-gray-400 mb-1">최우선매도</p>
<p id="askPrice1" class="font-semibold text-red-500">-원</p>
</div>
<div class="text-center">
<p class="text-xs text-gray-400 mb-1">최우선매수</p>
<p id="bidPrice1" class="font-semibold text-blue-500">-원</p>
</div>
</div>
</div>
<!-- 요약 그리드 -->
<div class="grid grid-cols-3 sm:grid-cols-6 gap-3 mt-5 pt-4 border-t border-gray-100 text-sm">
<div>
<p class="text-xs text-gray-400">시가</p>
<p id="openPrice" class="font-semibold text-gray-800">{{ formatPrice .Stock.Open }}원</p>
</div>
<div>
<p class="text-xs text-gray-400">고가</p>
<p id="highPrice" class="font-semibold text-red-500">{{ formatPrice .Stock.High }}원</p>
</div>
<div>
<p class="text-xs text-gray-400">저가</p>
<p id="lowPrice" class="font-semibold text-blue-500">{{ formatPrice .Stock.Low }}원</p>
</div>
<div>
<p class="text-xs text-gray-400">거래량</p>
<p id="volume" class="font-semibold text-gray-800">{{ formatPrice .Stock.Volume }}</p>
</div>
<div>
<p class="text-xs text-gray-400">거래대금</p>
<p id="tradeMoney" class="font-semibold text-gray-800">-</p>
</div>
<div>
<p class="text-xs text-gray-400">체결량</p>
<p id="tradeVolume" class="font-semibold text-gray-800">-</p>
</div>
</div>
<!-- 2행: 기준가/상하한가/체결시각 -->
<div class="grid grid-cols-3 sm:grid-cols-4 gap-3 mt-3 pt-3 border-t border-gray-50 text-sm">
<div>
<p class="text-xs text-gray-400">기준가</p>
<p id="basePrice" class="font-semibold text-gray-800">-</p>
</div>
<div>
<p class="text-xs text-gray-400">상한가</p>
<p id="upperLimit" class="font-semibold text-red-500">-</p>
</div>
<div>
<p class="text-xs text-gray-400">하한가</p>
<p id="lowerLimit" class="font-semibold text-blue-500">-</p>
</div>
<div>
<p class="text-xs text-gray-400">체결시각</p>
<p id="tradeTime" class="font-semibold text-gray-800">-</p>
</div>
</div>
</div>
<!-- 차트 + 호가창 (좌우 분할) -->
<div class="flex flex-col xl:flex-row gap-4">
<!-- 차트 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-5 border border-gray-100 flex-1 min-w-0">
<!-- 차트 탭 -->
<div class="flex gap-2 mb-4">
<button onclick="switchChart('daily')" id="tab-daily"
class="px-4 py-1.5 text-sm rounded-full bg-gray-100 text-gray-600 font-medium hover:bg-gray-200">
일봉
</button>
<button onclick="switchChart('minute1')" id="tab-minute1"
class="px-4 py-1.5 text-sm rounded-full bg-blue-500 text-white font-medium">
1분
</button>
<button onclick="switchChart('minute5')" id="tab-minute5"
class="px-4 py-1.5 text-sm rounded-full bg-gray-100 text-gray-600 font-medium hover:bg-gray-200">
5분
</button>
<button onclick="switchChart('tick')" id="tab-tick"
class="px-4 py-1.5 text-sm rounded-full bg-gray-100 text-gray-600 font-medium hover:bg-gray-200">
</button>
</div>
<!-- 이동평균선 범례 -->
<div class="flex gap-3 mb-2 text-xs">
<span class="flex items-center gap-1"><span class="inline-block w-4 bg-amber-400" style="height:1px"></span>MA20</span>
<span class="flex items-center gap-1"><span class="inline-block w-4 bg-emerald-500" style="height:1px"></span>MA60</span>
<span class="flex items-center gap-1"><span class="inline-block w-4 bg-violet-500" style="height:1px"></span>MA120</span>
</div>
<!-- 캔들 차트 컨테이너 (기본 표시) -->
<div id="chartContainer" class="w-full" style="height: 380px;"></div>
<!-- 틱 차트 컨테이너 (기본 숨김) -->
<div id="tickChartContainer" class="w-full" style="height: 380px; display: none;"></div>
</div>
<!-- 우측 컬럼: 로그인 시 호가창+주문+계좌, 비로그인 시 넓은 호가창만 -->
<div class="{{ if .LoggedIn }}xl:w-80{{ else }}xl:w-96{{ end }} shrink-0 flex flex-col gap-3">
<!-- 호가창 -->
<div class="bg-white rounded-xl shadow-sm p-4 border border-gray-100">
<div class="flex items-center justify-between mb-2">
<h2 class="text-sm font-semibold text-gray-800">호가창</h2>
<span id="askTime" class="text-xs text-gray-400">-</span>
</div>
<!-- 총잔량 헤더 -->
<div class="grid grid-cols-3 text-xs text-gray-400 mb-1 px-2">
<span class="text-right"><span id="totalAskVol" class="text-red-400 font-semibold">-</span></span>
<span class="text-center">호가</span>
<span class="text-left"><span id="totalBidVol" class="text-blue-400 font-semibold">-</span></span>
</div>
<table class="w-full">
<thead>
<tr class="text-xs text-gray-400 border-b border-gray-200">
<th class="py-1.5 px-2 text-right font-normal">매도잔량</th>
<th class="py-1.5 px-2 text-center font-normal">호가</th>
<th class="py-1.5 px-2 text-left font-normal">매수잔량</th>
</tr>
</thead>
<tbody id="orderbookBody">
<tr>
<td colspan="3" class="text-center py-6 text-xs text-gray-400">호가 데이터 수신 대기 중...</td>
</tr>
</tbody>
</table>
</div>
{{ if .LoggedIn }}
<!-- 주문창 패널 -->
<div class="bg-white rounded-xl shadow-sm p-4 border border-gray-100">
<!-- 매수/매도 탭 -->
<div class="flex gap-1 mb-3">
<button id="orderBuyTab" onclick="updateOrderSide('buy')"
class="flex-1 py-1.5 text-sm font-semibold rounded-lg bg-red-500 text-white transition-colors">
매수
</button>
<button id="orderSellTab" onclick="updateOrderSide('sell')"
class="flex-1 py-1.5 text-sm font-semibold rounded-lg bg-gray-100 text-gray-600 transition-colors">
매도
</button>
</div>
<!-- 거래소 구분 -->
<div class="flex gap-3 mb-3 text-xs">
<label class="flex items-center gap-1 cursor-pointer">
<input type="radio" name="orderExchange" value="KRX" checked class="accent-blue-500">
<span>KRX</span>
</label>
<label class="flex items-center gap-1 cursor-pointer">
<input type="radio" name="orderExchange" value="NXT" class="accent-blue-500">
<span>NXT</span>
</label>
</div>
<!-- 주문유형 -->
<div class="mb-3">
<label class="text-xs text-gray-400 block mb-1">주문유형</label>
<select id="orderTradeType" onchange="onTradeTypeChange()"
class="w-full text-xs border border-gray-200 rounded-lg px-2 py-1.5 focus:outline-none focus:border-blue-400">
<option value="0">지정가</option>
<option value="3">시장가</option>
<option value="6">최유리지정가</option>
<option value="7">최우선지정가</option>
<option value="5">조건부지정가</option>
</select>
</div>
<!-- 단가 입력 -->
<div class="mb-3">
<label class="text-xs text-gray-400 block mb-1">단가</label>
<div class="flex gap-1">
<input id="orderPrice" type="number" min="0" placeholder="단가 입력"
oninput="updateOrderTotal(); loadOrderable()"
class="flex-1 text-xs border border-gray-200 rounded-lg px-2 py-1.5 text-right focus:outline-none focus:border-blue-400">
<button id="priceUpBtn" onclick="adjustPrice(1)"
class="px-2 py-1 text-xs rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors"></button>
<button id="priceDownBtn" onclick="adjustPrice(-1)"
class="px-2 py-1 text-xs rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors"></button>
</div>
</div>
<!-- 수량 입력 -->
<div class="mb-3">
<label class="text-xs text-gray-400 block mb-1">수량</label>
<input id="orderQty" type="number" min="0" placeholder="수량 입력"
oninput="updateOrderTotal()"
class="w-full text-xs border border-gray-200 rounded-lg px-2 py-1.5 text-right focus:outline-none focus:border-blue-400">
<!-- 퍼센트 버튼 -->
<div class="flex gap-1 mt-1">
<button onclick="setQtyPercent(10)" class="flex-1 text-xs py-1 rounded border border-gray-200 hover:bg-gray-50">10%</button>
<button onclick="setQtyPercent(25)" class="flex-1 text-xs py-1 rounded border border-gray-200 hover:bg-gray-50">25%</button>
<button onclick="setQtyPercent(50)" class="flex-1 text-xs py-1 rounded border border-gray-200 hover:bg-gray-50">50%</button>
<button onclick="setQtyPercent(100)" class="flex-1 text-xs py-1 rounded border border-gray-200 hover:bg-gray-50">100%</button>
</div>
</div>
<!-- 총금액 + 주문가능 정보 -->
<div class="mb-3 text-xs space-y-1">
<div class="flex justify-between">
<span class="text-gray-400">총 주문금액</span>
<span id="orderTotal" class="font-semibold text-gray-800">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">주문가능현금</span>
<span id="orderableAmt" class="text-gray-600">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">예수금</span>
<span id="orderableEntr" class="text-gray-600">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">주문가능수량</span>
<span id="orderableQty" class="text-gray-600">-</span>
</div>
</div>
<!-- 확인 메시지 -->
<div id="orderConfirmBox" class="hidden mb-3 p-2 bg-gray-50 rounded-lg border border-gray-200 text-xs text-center">
<p id="orderConfirmMsg" class="text-gray-700 mb-2"></p>
<div class="flex gap-2">
<button onclick="hideOrderConfirm()"
class="flex-1 py-1.5 rounded border border-gray-300 text-gray-600 hover:bg-gray-100 text-xs">취소</button>
<button onclick="submitOrder()"
class="flex-1 py-1.5 rounded bg-blue-500 text-white hover:bg-blue-600 text-xs font-semibold">확인</button>
</div>
</div>
<!-- 주문 버튼 -->
<button id="orderSubmitBtn" onclick="showOrderConfirm()"
class="w-full py-2.5 text-sm font-bold rounded-lg bg-red-500 hover:bg-red-600 text-white transition-colors">
매수
</button>
</div>
<!-- 계좌 정보 탭 (미체결/체결/잔고) -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100">
<!-- 탭 헤더 -->
<div class="flex border-b border-gray-100 text-xs font-medium">
<button id="pendingTab" onclick="showAccountTab('pending')"
class="flex-1 py-2.5 text-center border-b-2 border-blue-500 text-blue-600 active-tab transition-colors">
미체결
</button>
<button id="historyTab" onclick="showAccountTab('history')"
class="flex-1 py-2.5 text-center text-gray-500 hover:text-gray-700 transition-colors">
체결
</button>
<button id="balanceTab" onclick="showAccountTab('balance')"
class="flex-1 py-2.5 text-center text-gray-500 hover:text-gray-700 transition-colors">
잔고
</button>
</div>
<!-- 탭 패널 -->
<div class="p-3 max-h-64 overflow-y-auto">
<div id="pendingPanel">
<p class="text-xs text-gray-400 text-center py-4">조회 중...</p>
</div>
<div id="historyPanel" class="hidden"></div>
<div id="balancePanel" class="hidden"></div>
</div>
</div>
{{ else }}
<!-- 비로그인: 로그인 유도 배너 -->
<div class="bg-gray-50 rounded-xl border border-gray-200 p-5 text-center">
<p class="text-sm text-gray-500 mb-3">주문 기능은 로그인 후 이용 가능합니다.</p>
<a href="/login?next={{ .Stock.Code }}"
class="inline-block px-5 py-2 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
로그인
</a>
</div>
{{ end }}
</div><!-- /우측 컬럼 -->
</div>
<!-- 프로그램 매매 + 체결강도 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- 프로그램 매매 -->
<div class="bg-white rounded-xl shadow-sm p-4 border border-gray-100">
<h2 class="text-sm font-semibold text-gray-800 mb-3">프로그램 매매</h2>
<div id="programTrading">
<span class="text-xs text-gray-400">프로그램 매매 데이터 수신 대기 중...</span>
</div>
</div>
<!-- 체결강도 & 예상체결 -->
<div class="bg-white rounded-xl shadow-sm p-4 border border-gray-100">
<h2 class="text-sm font-semibold text-gray-800 mb-3">체결 정보</h2>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="text-xs text-gray-400">체결강도</p>
<p id="cntrStr" class="font-semibold text-gray-800">{{ printf "%.1f" .Stock.CntrStr }}%</p>
</div>
<div>
<p class="text-xs text-gray-400">시가총액</p>
<p id="marketCap" class="font-semibold text-gray-800">-</p>
</div>
</div>
</div>
</div>
<!-- 최근 공시 -->
<div class="bg-white rounded-xl shadow-sm p-5 border border-gray-100">
<h2 class="text-base font-semibold text-gray-800 mb-4">최근 공시</h2>
<div id="disclosureLoading" class="text-center py-6 text-sm text-gray-400">공시 정보를 불러오는 중...</div>
<ul id="disclosureList" class="hidden divide-y divide-gray-100"></ul>
<div id="disclosureError" class="hidden text-center py-6 text-sm text-gray-400">공시 정보를 불러올 수 없습니다.</div>
<div id="disclosureEmpty" class="hidden text-center py-6 text-sm text-gray-400">최근 공시 내역이 없습니다.</div>
</div>
<!-- 관련 뉴스 -->
<div class="bg-white rounded-xl shadow-sm p-5 border border-gray-100">
<h2 class="text-base font-semibold text-gray-800 mb-4">관련 뉴스</h2>
<div id="newsLoading" class="text-center py-6 text-sm text-gray-400">뉴스를 불러오는 중...</div>
<ul id="newsList" class="hidden divide-y divide-gray-100"></ul>
<div id="newsError" class="hidden text-center py-6 text-sm text-gray-400">뉴스를 불러올 수 없습니다.</div>
<div id="newsEmpty" class="hidden text-center py-6 text-sm text-gray-400">관련 뉴스가 없습니다.</div>
</div>
</div>
<!-- 종목 코드를 JavaScript에 전달 -->
<script>
const STOCK_CODE = "{{ .Stock.Code }}";
const STOCK_NAME = "{{ .Stock.Name }}";
const STORAGE_KEY = 'watchlist_v1';
function loadWatchlist() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
catch { return []; }
}
function saveWatchlist(list) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
}
function isWatched() {
return loadWatchlist().some(s => s.code === STOCK_CODE);
}
function renderWatchlistBtn() {
const btn = document.getElementById('watchlistToggleBtn');
if (!btn) return;
if (isWatched()) {
btn.innerHTML = '★ 관심종목 해제';
btn.className = 'ml-auto flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium border transition-colors bg-yellow-50 text-yellow-600 border-yellow-300 hover:bg-yellow-100';
} else {
btn.innerHTML = '☆ 관심종목 추가';
btn.className = 'ml-auto flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium border transition-colors bg-white text-gray-500 border-gray-300 hover:bg-gray-50';
}
}
function toggleWatchlist() {
const list = loadWatchlist();
const idx = list.findIndex(s => s.code === STOCK_CODE);
if (idx >= 0) {
list.splice(idx, 1);
} else {
list.push({ code: STOCK_CODE, name: STOCK_NAME });
}
saveWatchlist(list);
renderWatchlistBtn();
}
renderWatchlistBtn();
</script>
{{ end }}
{{ define "scripts" }}
<script src="/static/js/orderbook.js"></script>
<script src="/static/js/chart.js"></script>
<script src="/static/js/disclosure.js"></script>
<script src="/static/js/news.js"></script>
{{ if .LoggedIn }}<script src="/static/js/order.js"></script>{{ end }}
{{ end }}

View File

@@ -0,0 +1,86 @@
{{ template "base.html" . }}
{{ define "sidebar" }}{{ end }}
{{ define "content" }}
<div class="space-y-4">
<!-- 헤더 + 필터 바 -->
<div class="flex flex-wrap items-center gap-3">
<h1 class="text-xl font-bold text-gray-800 flex items-center gap-2">
<span class="text-purple-500">📊</span> 테마 분석
</h1>
<!-- 날짜 구간 탭 -->
<div class="flex rounded-lg border border-gray-200 overflow-hidden text-xs font-medium">
<button data-date="1" class="date-tab px-3 py-1.5 bg-blue-500 text-white">1일</button>
<button data-date="5" class="date-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200">5일</button>
<button data-date="20" class="date-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200">20일</button>
</div>
<!-- 정렬 탭 -->
<div class="flex rounded-lg border border-gray-200 overflow-hidden text-xs font-medium">
<button data-sort="3" class="sort-tab px-3 py-1.5 bg-blue-500 text-white">등락률순</button>
<button data-sort="1" class="sort-tab px-3 py-1.5 bg-white text-gray-600 hover:bg-gray-50 border-l border-gray-200">기간수익률순</button>
</div>
<!-- 테마 검색 -->
<input id="themeSearch" type="text" placeholder="테마명 검색..."
class="text-xs px-3 py-1.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-400 w-36">
<span id="themeCount" class="text-xs text-gray-400 ml-auto"></span>
</div>
<!-- 2열 레이아웃: 테마 목록 + 구성종목 패널 -->
<div class="flex gap-5 items-start">
<!-- 좌: 테마 목록 -->
<div class="flex-1 min-w-0">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<!-- 테이블 헤더 -->
<div class="grid grid-cols-[2fr_1fr_80px_80px_80px_80px] gap-0 text-xs font-semibold text-gray-500 bg-gray-50 border-b border-gray-100 px-4 py-2.5">
<span data-col="name" class="theme-col-sort cursor-pointer select-none hover:text-blue-500 transition-colors">테마명 <span class="sort-arrow"></span></span>
<span>주력종목</span>
<span data-col="fluRt" class="theme-col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">등락률 <span class="sort-arrow"></span></span>
<span data-col="periodRt" class="theme-col-sort text-right cursor-pointer select-none hover:text-blue-500 transition-colors">기간수익률 <span class="sort-arrow"></span></span>
<span data-col="stockCount" class="theme-col-sort text-center cursor-pointer select-none hover:text-blue-500 transition-colors">종목수 <span class="sort-arrow"></span></span>
<span data-col="risingCount" class="theme-col-sort text-center cursor-pointer select-none hover:text-blue-500 transition-colors">상승/하락 <span class="sort-arrow"></span></span>
</div>
<!-- 테마 목록 -->
<div id="themeList" class="divide-y divide-gray-50">
<div class="px-4 py-10 text-center text-sm text-gray-400">데이터를 불러오는 중...</div>
</div>
</div>
</div>
<!-- 우: 구성종목 패널 (sticky) -->
<div id="themeDetailPanel"
class="w-80 shrink-0 sticky top-20 self-start bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<!-- 초기 안내 -->
<div id="themeDetailEmpty" class="px-6 py-12 text-center text-sm text-gray-400">
<p class="text-2xl mb-2">👈</p>
<p>테마를 클릭하면<br>구성종목을 확인할 수 있습니다.</p>
</div>
<!-- 구성종목 내용 (로드 후 표시) -->
<div id="themeDetailContent" class="hidden">
<div class="px-4 py-3 border-b border-gray-100 bg-gray-50">
<p id="detailThemeName" class="font-bold text-gray-800 text-sm truncate"></p>
<div class="flex items-center gap-3 mt-1 text-xs">
<span>등락률 <span id="detailFluRt" class="font-semibold"></span></span>
<span>기간수익률 <span id="detailPeriodRt" class="font-semibold text-purple-600"></span></span>
</div>
</div>
<div id="detailStockList" class="divide-y divide-gray-50 max-h-[70vh] overflow-y-auto"></div>
</div>
<div id="themeDetailLoading" class="hidden px-6 py-10 text-center text-sm text-gray-400">
<div class="animate-pulse">조회 중...</div>
</div>
</div>
</div>
</div>
{{ end }}
{{ define "scripts" }}
<script src="/static/js/theme.js"></script>
{{ end }}