408 lines
21 KiB
HTML
408 lines
21 KiB
HTML
{{ 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 }}
|