auto: 일일 백업 2026-07-24 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1747,6 +1747,31 @@ class MultipleMatchError(Exception):
|
||||
super().__init__(query)
|
||||
|
||||
|
||||
_MARKET_BY_CODE_CACHE = None
|
||||
|
||||
|
||||
def _market_label_html(code: str) -> str:
|
||||
"""종목코드 → 시장(코스피/코스닥) 작은 배지 HTML. 미상·ETF 등이면 ''.
|
||||
stock_codes.json(name→{code,market})을 code→market로 뒤집어 프로세스 1회 캐시."""
|
||||
global _MARKET_BY_CODE_CACHE
|
||||
code = (code or '').strip()
|
||||
if not code:
|
||||
return ''
|
||||
if _MARKET_BY_CODE_CACHE is None:
|
||||
import kiwoom_client as kc
|
||||
_MARKET_BY_CODE_CACHE = {
|
||||
info['code']: info['market']
|
||||
for info in kc._load_code_cache().values()
|
||||
if info.get('code') and info.get('market')
|
||||
}
|
||||
mk = _MARKET_BY_CODE_CACHE.get(code)
|
||||
if mk == 'KOSPI':
|
||||
return '<span class="detail-market mkt-kospi">KOSPI</span>'
|
||||
if mk == 'KOSDAQ':
|
||||
return '<span class="detail-market mkt-kosdaq">KOSDAQ</span>'
|
||||
return ''
|
||||
|
||||
|
||||
def _resolve_stock_fuzzy(query: str) -> dict:
|
||||
"""정확 매칭(case-sensitive) → substring(case-insensitive) 단일 → 다중이면 후보 응답.
|
||||
'lg' 입력 시 'LG' 단독 자동 통과하지 않고 'LG전자' 등도 함께 후보로 노출되도록
|
||||
@@ -2160,30 +2185,51 @@ def _write_chart_hlines(d: dict) -> None:
|
||||
|
||||
|
||||
def _chart_hlines_ui(code: str, hlines: list) -> str:
|
||||
"""차트 하단 수평선 관리 UI — 등록된 선 칩(삭제 ×) + 가격 입력 폼.
|
||||
"""차트 하단 수평선 UI — 컴팩트 행(등록된 선 색상 칩 + '+' 버튼).
|
||||
'+' 클릭 시 추가·삭제 모달. 색상은 정렬 인덱스로 SVG와 동일 매핑.
|
||||
/api/chart_svg 응답 HTML 블록에 포함돼 모든 차트(보유·관심·감시)에 동일하게 노출."""
|
||||
code_s = (code or '').strip()
|
||||
if not code_s:
|
||||
return ''
|
||||
import stock_analysis as sa
|
||||
colors = getattr(sa, 'HLINE_COLORS', None) or ['#e879f9']
|
||||
code_attr = html.escape(code_s, quote=True)
|
||||
if hlines:
|
||||
chips = ''.join(
|
||||
f'<span class="hl-chip">{p:,}'
|
||||
f'<button type="button" class="hl-chip-x" data-hl-remove="{p}" aria-label="삭제">×</button>'
|
||||
f'</span>'
|
||||
for p in hlines
|
||||
f'<span class="hl-chip" style="color:{colors[i % len(colors)]};'
|
||||
f'border-color:{colors[i % len(colors)]}">{p:,}</span>'
|
||||
for i, p in enumerate(hlines)
|
||||
)
|
||||
manage = ''.join(
|
||||
f'<div class="hl-mrow">'
|
||||
f'<span class="hl-swatch" style="background:{colors[i % len(colors)]}"></span>'
|
||||
f'<span class="hl-mprice">{p:,}</span>'
|
||||
f'<button type="button" class="hl-mdel" data-hl-remove="{p}" aria-label="삭제">삭제</button>'
|
||||
f'</div>'
|
||||
for i, p in enumerate(hlines)
|
||||
)
|
||||
else:
|
||||
chips = '<span class="hl-empty">등록된 수평선이 없어요</span>'
|
||||
chips = '<span class="hl-empty">수평선 없음</span>'
|
||||
manage = '<div class="hl-empty">등록된 수평선이 없어요</div>'
|
||||
return (
|
||||
f'<div class="chart-hlines" data-hl-code="{code_attr}">'
|
||||
'<div class="hl-title">수평선</div>'
|
||||
'<div class="hl-row">'
|
||||
f'<div class="hl-list">{chips}</div>'
|
||||
'<button type="button" class="hl-add-open" data-hl-open aria-label="수평선 추가·관리">+</button>'
|
||||
'</div>'
|
||||
'<div class="hl-modal hidden" data-hl-modal>'
|
||||
'<div class="hl-modal-ov" data-hl-close></div>'
|
||||
'<div class="hl-modal-box">'
|
||||
'<div class="hl-modal-head"><span>수평선</span>'
|
||||
'<button type="button" class="hl-modal-x" data-hl-close aria-label="닫기">×</button></div>'
|
||||
'<form class="chart-hlines-add">'
|
||||
'<input type="number" name="price" inputmode="numeric" step="1" min="1" '
|
||||
'placeholder="가격 입력" autocomplete="off">'
|
||||
'<button type="submit" class="hl-add-btn">+ 추가</button>'
|
||||
'<button type="submit" class="hl-add-btn">추가</button>'
|
||||
'</form>'
|
||||
f'<div class="hl-manage-list">{manage}</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
@@ -2736,6 +2782,9 @@ def _render_row(c: dict, source: str = 'watchlist') -> str:
|
||||
if not is_pending:
|
||||
code_span = f'<span class="detail-code">{code}</span>' if code else ''
|
||||
detail_name_html = f'<div class="detail-name">{stock}{code_span}{tag_add_btn}{group_add_btn}</div>'
|
||||
# 현재가 아래 메타 라인 — 관심·감시는 비중·계좌 없어 시장(KOSPI/KOSDAQ)만.
|
||||
_mkt_label = _market_label_html(code)
|
||||
market_meta_html = f'<div class="detail-meta">{_mkt_label}</div>' if _mkt_label else ''
|
||||
|
||||
if source == 'interests':
|
||||
def _num_attr(field, sub):
|
||||
@@ -2814,6 +2863,7 @@ def _render_row(c: dict, source: str = 'watchlist') -> str:
|
||||
<div class="detail">
|
||||
{detail_name_html}
|
||||
{f'<div class="detail-simple"><span class="ds-lbl">현재가</span><span class="ds-val">{price_cell}</span></div>' if price is not None else ''}
|
||||
{market_meta_html}
|
||||
{'<div class="alert-notice">최초 알림 완료 — 리셋 전까지 매수 알림 없음</div>' if unheld_done else ''}
|
||||
<div class="grid2">
|
||||
{held_row}
|
||||
@@ -3543,6 +3593,7 @@ def _render_holding_row(r: dict, total_value: int, show_day_change: bool = False
|
||||
<div class="detail-name">{stock}{f'<span class="detail-code">{code}</span>' if code else ''}<span class="detail-owner owner-{owner_cls}">{owner_disp}</span>{tag_add_btn}</div>
|
||||
{f'<div class="detail-tag">{tag_chip}</div>' if tag_chip else ''}
|
||||
{f'<div class="detail-simple"><span class="ds-lbl">현재가</span><span class="ds-val">{price_html}</span></div>' if price else ''}
|
||||
<div class="detail-meta"><span class="dm-item">비중 {weight:.2f}%</span><span class="dm-item">{acct_label}</span>{_market_label_html(code)}</div>
|
||||
<dl>{dl_inner}</dl>
|
||||
<dl class="pl-summary">{pl_dl_inner}</dl>
|
||||
{ohlc_mini_html}
|
||||
@@ -5030,6 +5081,12 @@ details.row + .section-label { margin-top: 14px; padding-top: 24px; border-top-c
|
||||
/* 펼친 detail의 태그는 크게 (미리보기·관심행 칩은 작게 유지). */
|
||||
.detail-name { font-size: 15px; font-weight: 700; color: #f0f0f0; margin: 0 0 8px; display: flex; align-items: baseline; gap: 8px; }
|
||||
.detail-name .detail-code { font-size: 11px; font-weight: 500; color: #8b8f9a; font-variant-numeric: tabular-nums; }
|
||||
.detail-market { align-self: center; font-size: 9px; font-weight: 500; color: #6b7280; }
|
||||
/* 현재가 아래 메타 라인 — 비중 · 계좌명 · 시장 (작은 회색, · 구분) */
|
||||
.detail .detail-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: -4px 0 10px; font-size: 11px; color: #8b8f9a; }
|
||||
.detail .detail-meta .dm-item { font-weight: 500; }
|
||||
.detail .detail-meta > * + *::before { content: "·"; margin-right: 8px; color: #4b5563; }
|
||||
.detail .detail-meta .detail-market { font-size: 11px; }
|
||||
.detail-name .detail-owner { font-size: 11px; font-weight: 600; border-radius: 4px; padding: 1px 6px; align-self: center; }
|
||||
.detail-name .detail-owner.owner-self { color: #6aa9ff; background: rgba(106,169,240,0.12); border: 1px solid rgba(106,169,240,0.4); }
|
||||
.detail-name .detail-owner.owner-gahee { color: #e879f9; background: rgba(232,121,249,0.12); border: 1px solid rgba(232,121,249,0.4); }
|
||||
@@ -5091,8 +5148,8 @@ details.row[open] > .detail {
|
||||
}
|
||||
/* 팝업 하단 sticky 액션 바 (JS가 열린 .detail 의 .actions 를 맨 끝으로 옮기고 이 클래스 부여) — 메모·상세·거래내역·거래 + 닫기, 항상 노출 */
|
||||
.detail .actions.detail-actions-footer { position: sticky; bottom: 0; z-index: 3; margin: 8px -16px 0; padding: 8px 16px 10px; gap: 6px; background: #0d1018; border-top: 1px solid #1f2330; }
|
||||
/* 닫기 버튼 — 하단 바 마지막 항목. 우측으로 밀어 다른 버튼과 구분 */
|
||||
.detail .actions .btn-detail-close { margin-left: auto; color: #e6e9ef; border-color: #3a4154; }
|
||||
/* 닫기 버튼 — 하단 바 마지막 항목. 전체 너비 한 줄로 (자기 행 차지) */
|
||||
.detail .actions .btn-detail-close { flex: 1 1 100%; padding-top: 11px; padding-bottom: 11px; color: #e6e9ef; border-color: #3a4154; }
|
||||
.detail .actions .btn-detail-close:hover { background: #22262f; border-color: #6aa9ff; color: #fff; }
|
||||
/* 종목 변경 드롭다운 — 하단 바 버튼 위(자기 행 차지) */
|
||||
.detail .actions .detail-stock-switch { flex: 1 1 100%; min-width: 0; padding: 7px 10px; background: #0a0d12; color: #e6e9ef; border: 1px solid #2a3142; border-radius: 6px; font-family: inherit; font-size: 14px; }
|
||||
@@ -5181,18 +5238,34 @@ details.row[open] > .detail {
|
||||
.detail .block.chart-svg .chart-subtabs button { padding: 3px 9px; background: transparent; color: #7a8493; border: 1px solid #2a2f3a; border-radius: 5px; cursor: pointer; font-size: 11px; font-weight: 600; font-family: inherit; }
|
||||
.detail .block.chart-svg .chart-subtabs button:hover { color: #cfd5df; border-color: #3f4654; }
|
||||
.detail .block.chart-svg .chart-subtabs button.active { background: rgba(52,211,153,0.10); color: #34d399; border-color: rgba(52,211,153,0.45); }
|
||||
.detail .block.chart-svg .chart-hlines { margin-top: 8px; padding: 8px; background: #131820; border: 1px solid #2a2f3a; border-radius: 8px; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-title { font-size: 11px; font-weight: 700; color: #e879f9; margin-bottom: 6px; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
||||
/* 차트 하단 수평선 컴팩트 행 — 색상 칩 나열 + '+' 버튼 (추가·관리는 모달) */
|
||||
.detail .block.chart-svg .chart-hlines { margin-top: 8px; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-row { display: flex; align-items: center; gap: 6px; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-list { display: flex; flex-wrap: wrap; gap: 5px; flex: 1; min-width: 0; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-empty { font-size: 11px; color: #7a8493; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 6px 3px 9px; background: rgba(232,121,249,0.12); color: #e879f9; border: 1px solid rgba(232,121,249,0.45); border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-chip-x { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; background: transparent; color: #e879f9; border: none; border-radius: 50%; cursor: pointer; font-size: 14px; line-height: 1; font-family: inherit; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-chip-x:hover { background: rgba(232,121,249,0.25); }
|
||||
.detail .block.chart-svg .chart-hlines-add { display: flex; gap: 6px; }
|
||||
.detail .block.chart-svg .chart-hlines-add input { flex: 1; min-width: 0; padding: 6px 10px; background: #0a0d12; color: #e6e9ef; border: 1px solid #2a2f3a; border-radius: 6px; font-size: 16px; font-family: inherit; }
|
||||
.detail .block.chart-svg .chart-hlines-add input:focus { outline: none; border-color: #e879f9; }
|
||||
.detail .block.chart-svg .chart-hlines-add .hl-add-btn { padding: 6px 14px; background: rgba(232,121,249,0.14); color: #e879f9; border: 1px solid rgba(232,121,249,0.5); border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 700; font-family: inherit; white-space: nowrap; }
|
||||
.detail .block.chart-svg .chart-hlines-add .hl-add-btn:hover { background: rgba(232,121,249,0.24); }
|
||||
.detail .block.chart-svg .chart-hlines .hl-chip { display: inline-flex; align-items: center; padding: 2px 9px; background: rgba(255,255,255,0.04); border: 1px solid; border-radius: 12px; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-add-open { flex: none; width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; padding: 0; background: rgba(232,121,249,0.14); color: #e879f9; border: 1px solid rgba(232,121,249,0.5); border-radius: 8px; font-size: 20px; font-weight: 700; line-height: 1; cursor: pointer; font-family: inherit; }
|
||||
.detail .block.chart-svg .chart-hlines .hl-add-open:hover { background: rgba(232,121,249,0.24); }
|
||||
/* 수평선 추가·관리 모달 (position:fixed 로 상세 팝업 위에 뜸) */
|
||||
.hl-modal { position: fixed; inset: 0; z-index: 1200; display: flex; align-items: center; justify-content: center; padding: 40px 16px; }
|
||||
.hl-modal.hidden { display: none; }
|
||||
.hl-modal .hl-modal-ov { position: absolute; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
|
||||
.hl-modal .hl-modal-box { position: relative; width: 100%; max-width: 320px; max-height: 70vh; overflow-y: auto; background: #0d1018; border: 1px solid #2f3543; border-radius: 12px; padding: 14px 16px; box-shadow: 0 18px 50px rgba(0,0,0,0.55); }
|
||||
.hl-modal .hl-modal-head { display: flex; align-items: center; justify-content: space-between; font-size: 13px; font-weight: 700; color: #f0f0f0; margin-bottom: 12px; }
|
||||
.hl-modal .hl-modal-x { width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; padding: 0; background: #1a1d26; color: #e6e9ef; border: 1px solid #2f3543; border-radius: 50%; font-size: 16px; line-height: 1; cursor: pointer; font-family: inherit; }
|
||||
.hl-modal .hl-modal-x:hover { background: #22262f; }
|
||||
.hl-modal .chart-hlines-add { display: flex; gap: 6px; margin-bottom: 12px; }
|
||||
.hl-modal .chart-hlines-add input { flex: 1; min-width: 0; padding: 8px 10px; background: #0a0d12; color: #e6e9ef; border: 1px solid #2a2f3a; border-radius: 6px; font-size: 16px; font-family: inherit; }
|
||||
.hl-modal .chart-hlines-add input:focus { outline: none; border-color: #e879f9; }
|
||||
.hl-modal .chart-hlines-add .hl-add-btn { padding: 8px 14px; background: rgba(232,121,249,0.14); color: #e879f9; border: 1px solid rgba(232,121,249,0.5); border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 700; font-family: inherit; white-space: nowrap; }
|
||||
.hl-modal .chart-hlines-add .hl-add-btn:hover { background: rgba(232,121,249,0.24); }
|
||||
.hl-modal .hl-manage-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.hl-modal .hl-mrow { display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: #131820; border: 1px solid #1f2330; border-radius: 6px; }
|
||||
.hl-modal .hl-swatch { flex: none; width: 14px; height: 14px; border-radius: 3px; }
|
||||
.hl-modal .hl-mprice { flex: 1; font-size: 13px; font-weight: 600; color: #e6e9ef; font-variant-numeric: tabular-nums; }
|
||||
.hl-modal .hl-mdel { padding: 4px 10px; background: transparent; color: #ff8a96; border: 1px solid rgba(255,77,94,0.32); border-radius: 6px; font-size: 12px; cursor: pointer; font-family: inherit; }
|
||||
.hl-modal .hl-mdel:hover { background: rgba(255,77,94,0.1); }
|
||||
.hl-modal .hl-empty { font-size: 12px; color: #7a8493; text-align: center; padding: 8px; }
|
||||
.detail .block.chart-svg .chart-subpanel { min-height: 200px; }
|
||||
|
||||
.actions {
|
||||
@@ -8774,10 +8847,12 @@ def render_html() -> str:
|
||||
# net-chart-tip 의 .tip-close 와 동일 패턴(팝업 자체 우상단).
|
||||
# ov 는 pointerEvents:none 이라 closeG 만 auto 로 override 해 클릭 통과.
|
||||
'var closeG=_mk("g",{"class":"crosshair-close"});'
|
||||
# 투명 히트영역 — 보이는 원(r=14)보다 크게(r=26) 덧대 터치 타겟 확대. 손가락이 살짝 빗나가도 인식.
|
||||
'var closeHit=_mk("circle",{cx:"0",cy:"0",r:"26",fill:"transparent"});'
|
||||
'var closeBg=_mk("circle",{cx:"0",cy:"0",r:"14",fill:"rgba(15,20,25,0.95)",stroke:"#cfd5df","stroke-width":"1.2"});'
|
||||
'var closeTxt=_mk("text",{x:"0",y:"1","text-anchor":"middle","dominant-baseline":"central","font-size":"22","font-weight":"700",fill:"#cfd5df"});'
|
||||
'closeTxt.textContent="×";'
|
||||
'closeG.appendChild(closeBg);closeG.appendChild(closeTxt);'
|
||||
'closeG.appendChild(closeHit);closeG.appendChild(closeBg);closeG.appendChild(closeTxt);'
|
||||
'closeG.style.cursor="pointer";'
|
||||
'closeG.style.pointerEvents="auto";'
|
||||
# xTipBg 320×176 안의 우상단 모서리. r=14 라 여백 18 두고 중심 (300,20).
|
||||
@@ -8963,13 +9038,43 @@ def render_html() -> str:
|
||||
'var subpanel=panel.querySelector(".chart-subpanel");'
|
||||
'if(subpanel)fetchMinute(code,unit,sr,subpanel);'
|
||||
'},false);'
|
||||
# 수평선(hlines) 추가/제거 — 저장 후 dailyCache 무효화 + 재fetch로 차트·칩 갱신.
|
||||
# 수평선(hlines) 추가/제거 — 저장 후 dailyCache 무효화 + 재fetch로 차트·칩 갱신. promise 반환.
|
||||
'function refreshChart(code){'
|
||||
'if(!code)return;'
|
||||
'if(!code)return null;'
|
||||
'dailyCache.delete(code);'
|
||||
'var pr=null;'
|
||||
'document.querySelectorAll(\'.chart-svg[data-chart-code="\'+Q(code)+\'"]\').forEach(function(el){'
|
||||
'if(el.closest("details[open]"))fetchDaily(code,el);'
|
||||
'if(el.closest("details[open]"))pr=fetchDaily(code,el);'
|
||||
'});'
|
||||
'return pr;'
|
||||
'}'
|
||||
# 재fetch 후 새로 그려진 모달을 다시 연다 (추가·삭제 반복 편의).
|
||||
'function reopenHl(code){'
|
||||
'var p=refreshChart(code);'
|
||||
'if(p&&p.then){p.then(function(){openHl(code);});}else{openHl(code);}'
|
||||
'}'
|
||||
# 포탈된 모달 제거. code 지정 시 해당 종목만, 없으면 전부(카드 닫힐 때).
|
||||
'function closeHlPortals(code){'
|
||||
'var sel=code?(\'.hl-modal[data-hl-portal="\'+Q(code)+\'"]\'):".hl-modal[data-hl-portal]";'
|
||||
'document.querySelectorAll(sel).forEach(function(m){m.remove();});'
|
||||
'}'
|
||||
# 모달 열기 — 상세 카드는 transform 조상이라 내부 position:fixed 가 뷰포트 기준이 안 됨.
|
||||
# 그래서 모달 clone 을 document.body 로 옮겨(포탈) 뷰포트 전체 오버레이로 띄운다. 원본은 블록에 hidden 유지.
|
||||
'function openHl(code){'
|
||||
'if(!code)return;'
|
||||
'closeHlPortals(code);'
|
||||
'var src=document.querySelector(\'.chart-hlines[data-hl-code="\'+Q(code)+\'"] [data-hl-modal]\');'
|
||||
'if(!src)return;'
|
||||
'var clone=src.cloneNode(true);'
|
||||
'clone.setAttribute("data-hl-portal",code);'
|
||||
'clone.classList.remove("hidden");'
|
||||
'document.body.appendChild(clone);'
|
||||
'}'
|
||||
# 이벤트 대상에서 종목코드 추출 — 블록(.chart-hlines) 또는 포탈된 모달(data-hl-portal) 양쪽 지원.
|
||||
'function hlCodeOf(el){'
|
||||
'var w=el.closest(".chart-hlines");if(w)return w.getAttribute("data-hl-code");'
|
||||
'var m=el.closest("[data-hl-modal]");if(m)return m.getAttribute("data-hl-portal");'
|
||||
'return null;'
|
||||
'}'
|
||||
'function postHline(url,code,price,cb){'
|
||||
'fetch(url,{method:"POST",credentials:"same-origin",'
|
||||
@@ -8983,21 +9088,26 @@ def render_html() -> str:
|
||||
'var f=ev.target;'
|
||||
'if(!f.classList||!f.classList.contains("chart-hlines-add"))return;'
|
||||
'ev.preventDefault();'
|
||||
'var wrap=f.closest(".chart-hlines");if(!wrap)return;'
|
||||
'var code=wrap.getAttribute("data-hl-code");'
|
||||
'var code=hlCodeOf(f);if(!code)return;'
|
||||
'var inp=f.querySelector(\'input[name="price"]\');'
|
||||
'var price=inp?inp.value.trim():"";'
|
||||
'if(!code||!price)return;'
|
||||
'postHline("/chart/hlines/add",code,price,function(){if(inp)inp.value="";refreshChart(code);});'
|
||||
'if(!price)return;'
|
||||
'postHline("/chart/hlines/add",code,price,function(){if(inp)inp.value="";reopenHl(code);});'
|
||||
'});'
|
||||
'document.addEventListener("click",function(ev){'
|
||||
# '+' 버튼 → 모달 열기
|
||||
'var op=ev.target.closest("[data-hl-open]");'
|
||||
'if(op){var w0=op.closest(".chart-hlines");if(w0){ev.preventDefault();openHl(w0.getAttribute("data-hl-code"));}return;}'
|
||||
# 모달 닫기 (오버레이·×) — 포탈 clone 은 제거
|
||||
'var cl=ev.target.closest("[data-hl-close]");'
|
||||
'if(cl){var m=cl.closest("[data-hl-modal]");if(m){ev.preventDefault();if(m.getAttribute("data-hl-portal")){m.remove();}else{m.classList.add("hidden");}}return;}'
|
||||
# 수평선 삭제
|
||||
'var b=ev.target.closest("[data-hl-remove]");if(!b)return;'
|
||||
'var wrap=b.closest(".chart-hlines");if(!wrap)return;'
|
||||
'ev.preventDefault();'
|
||||
'var code=wrap.getAttribute("data-hl-code");'
|
||||
'var code=hlCodeOf(b);if(!code)return;'
|
||||
'var price=b.getAttribute("data-hl-remove");'
|
||||
'if(!code||!price)return;'
|
||||
'postHline("/chart/hlines/remove",code,price,function(){refreshChart(code);});'
|
||||
'if(!price)return;'
|
||||
'postHline("/chart/hlines/remove",code,price,function(){reopenHl(code);});'
|
||||
'});'
|
||||
# 첫 페이지 로드 시 이미 열려있는 카드 (드물지만 안전).
|
||||
'if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",function(){loadChartsInOpen();});'
|
||||
@@ -10330,6 +10440,8 @@ def render_html() -> str:
|
||||
'var bd=null;'
|
||||
'function closeAll(){'
|
||||
'document.querySelectorAll("details.row[open]").forEach(function(d){d.removeAttribute("open");});'
|
||||
# 포탈된 수평선 모달도 함께 정리 (body 직속이라 카드 닫혀도 안 사라짐).
|
||||
'document.querySelectorAll(".hl-modal[data-hl-portal]").forEach(function(m){m.remove();});'
|
||||
'sync();'
|
||||
'}'
|
||||
'function anyOpen(){return !!document.querySelector("details.row[open]");}'
|
||||
@@ -10397,7 +10509,7 @@ def render_html() -> str:
|
||||
# 닫기 버튼 — 마지막 항목 (중복 방지)
|
||||
'if(!act.querySelector(":scope > .btn-detail-close")){'
|
||||
'var x=document.createElement("button");x.type="button";x.className="btn-detail-close";'
|
||||
'x.setAttribute("aria-label","닫기");x.textContent="✕ 닫기";'
|
||||
'x.setAttribute("aria-label","닫기");x.textContent="닫기";'
|
||||
'x.addEventListener("click",closeAll);'
|
||||
'act.appendChild(x);'
|
||||
'}'
|
||||
|
||||
Reference in New Issue
Block a user