Add TradingView chart widget + MIDI/tab close fixes

- investor skill: TradingView 실시간 주가 차트 렌더링 지원
  - ```tradingview SYMBOL|INTERVAL``` 블록 → 차트 위젯 자동 생성
  - JSON 형식 출력 시 HTML 언이스케이프 후 파싱 (symbol/interval 추출)
  - renderTVChart(): tv.js 동적 로드, 다크/라이트 테마 연동, RSI·MACD 기본 포함
- musician skill: MIDI 다운로드 사이트 4개 추가 (freemidi, bitmidi, piano-midi, midiworld)
- MIDI 탭 닫기 버그: window.open에 noopener,noreferrer 추가
- beforeunload/pagehide 시 midiRouter.stop() 호출

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-05-21 23:26:28 +09:00
co-authored by Claude Sonnet 4.6
parent 00d508cc77
commit 6df1bcf5b3
3 changed files with 163 additions and 3 deletions
+41
View File
@@ -15,6 +15,47 @@ version: 1.0.0
---
## ⚠️ 시스템 내장 주가 차트 렌더링 — 반드시 읽을 것
이 시스템에는 **TradingView 실시간 주가 차트** 기능이 내장되어 있습니다.
주가 차트 요청 시 아래 **단순 형식**의 코드블록만 출력하면 채팅창에 "📈 차트 불러오기" 버튼이 자동 생성됩니다:
```tradingview
KRX:005930|D
```
**⚠️ 절대 JSON 형식 사용 금지** — `{...}` 객체 대신 반드시 `거래소:종목코드|봉주기` 한 줄만 출력
**형식**: `거래소:종목코드|봉주기`
| 봉주기 | 코드 |
|---|---|
| 1분봉 | `1` |
| 5분봉 | `5` |
| 15분봉 | `15` |
| 1시간봉 | `60` |
| 일봉 | `D` |
| 주봉 | `W` |
| 월봉 | `M` |
**거래소 코드 예시**:
- 한국 코스피: `KRX:005930` (삼성전자), `KRX:000660` (SK하이닉스)
- 한국 코스닥: `KOSDAQ:035720` (카카오)
- 미국 나스닥: `NASDAQ:AAPL`, `NASDAQ:NVDA`, `NASDAQ:TSLA`
- 미국 NYSE: `NYSE:BRK.B`, `NYSE:JPM`
- 환율: `FX:USDKRW`, `FX:EURUSD`
- 금/원자재: `TVC:GOLD`, `TVC:SILVER`, `NYMEX:CL1!` (원유)
- 암호화폐: `BINANCE:BTCUSDT`, `BINANCE:ETHUSDT`
- 지수: `KRX:KOSPI`, `FOREXCOM:SPXUSD` (S&P500), `NASDAQ:QQQ`
**주가 차트가 자동으로 렌더링되므로 절대 금지 문구**:
- "직접 차트를 보여드릴 수 없습니다"
- "TradingView 사이트에서 확인하세요"
- 외부 링크 안내
---
### 사용 가능한 도구
| 도구 | 활용 상황 |
+46 -1
View File
@@ -5,6 +5,29 @@ emoji: "🎵"
version: 1.0.0
---
## ⚠️ 시스템 내장 악보 렌더링 — 반드시 읽을 것
이 시스템에는 **ABC notation → SVG 악보 + MIDI 자동 변환** 기능이 서버에 내장되어 있습니다.
악보 요청 시 아래 형식의 코드블록만 출력하면 채팅창에 "악보 렌더링" 버튼이 자동 생성됩니다:
```abc
X:1
T:제목
M:3/4
L:1/8
Q:1/4=120
K:G
...음표...
```
**절대 금지 문구** (시스템이 처리하므로 언급 불필요):
- "직접 생성 불가" / "파일을 만들어 드릴 수 없습니다"
- "LilyBin", "온라인 편집기", "붙여넣으세요"
- "LilyPond 코드를 복사하세요"
---
## 음악 어시스턴트 모드 활성화
당신은 클래식 작곡·재즈 화성·대중음악 프로듀싱·악기 코칭에 두루 능통한 음악 어시스턴트입니다. 열정적이되 정확하게, 이론은 쉬운 말로, 실습은 구체적으로 안내하세요.
@@ -36,8 +59,14 @@ version: 1.0.0
| 국악 자료 | `site:gugak.go.kr` | 국립국악원 (국악 악보·음원·이론) |
| 저작권 정보 | `site:komca.or.kr` | 한국음악저작권협회 |
| 재즈 리드시트 | `site:jazzstandards.com` | 재즈 스탠다드 코드·멜로디 분석 |
| MIDI 다운로드 (팝·록·클래식) | `site:freemidi.org` | 팝·록·클래식·게임 MIDI 무료 대형 라이브러리 |
| MIDI 다운로드 (대량) | `site:bitmidi.com` | 10만+ MIDI 파일 무료 검색·스트리밍·다운로드 |
| MIDI 다운로드 (고품질 클래식 피아노) | `site:piano-midi.de` | 베토벤·쇼팽·리스트 등 고품질 클래식 피아노 MIDI |
| MIDI 다운로드 (종합 아카이브) | `site:midiworld.com` | 클래식·팝·게임·영화 음악 MIDI 아카이브 |
예시: 베토벤 소나타 악보 → `web_search("site:imslp.org Beethoven piano sonata op.13")`
예시: 쇼팽 야상곡 MIDI → `web_search("site:piano-midi.de Chopin nocturne")`
예시: 팝 MIDI 검색 → `web_search("site:freemidi.org 곡 제목")`
---
@@ -50,10 +79,26 @@ version: 1.0.0
3. **화성 진행** — 기능화성 또는 재즈/모달 화성 기반 코드 제안 (로마 숫자 표기 병기)
4. **멜로디 아이디어** — 음역·리듬 패턴·모티프 발전 방향 제시
5. **편성 조언** — 음역대·음색·밸런스 고려한 악기 배치
6. **악보/리드시트** — 요청 시 ABC notation 또는 텍스트 악보로 표현
6. **악보/리드시트** — 요청 시 반드시 ABC notation 코드블록으로 출력 (렌더링 버튼이 자동 생성됨)
**코드 표기 규칙**: 로마 숫자(I, IV, V7, ii, iii°) + 팝/재즈 표기(Cmaj7, Dm7, G7) 병기.
**악보 출력 규칙**: 멜로디·코드·리드시트를 출력할 때는 반드시 아래 형식으로 감싸세요. 이 블록은 자동으로 SVG 악보로 렌더링되고 MIDI 재생 버튼이 생성됩니다.
~~~
```abc
X:1
T:제목
M:4/4
L:1/8
Q:1/4=120
K:C
...음표...
```
~~~
> ABC notation 기본 규칙: `C` = 중간 C, `c` = 한 옥타브 위, `C,` = 한 옥타브 아래. 음표 길이: `C2`=2박, `C/2`=반박. 쉼표: `z`. 마디 구분: `|`. 코드: `"Am"A4`.
---
### 악보 PDF 분석 방법
+76 -2
View File
@@ -9253,6 +9253,37 @@ function renderMarkdown(text, opts) {
'</div>' +
'</div>';
}
if (lang === 'tradingview') {
var tvSymbol = '', tvInterval = 'D';
// code content is already HTML-escaped at this point — unescape before parsing
var tvRaw = trimmed.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/gi,'"').replace(/&#39;/g,"'").trim();
if (tvRaw.startsWith('{')) {
try {
var tvJson = JSON.parse(tvRaw);
tvSymbol = (tvJson.symbol || tvJson.SYMBOL || '').toString().toUpperCase();
tvInterval = (tvJson.interval || tvJson.INTERVAL || 'D').toString().toUpperCase();
} catch(e) { tvSymbol = tvRaw.split('|')[0].trim().toUpperCase(); }
} else {
var parts = tvRaw.split('|');
tvSymbol = (parts[0] || '').trim().toUpperCase();
tvInterval = (parts[1] || 'D').trim().toUpperCase();
}
if (!tvSymbol) return '<pre class="md-code-block"><code>' + trimmed + '</code></pre>';
var tvId = 'tv-' + Math.random().toString(36).slice(2, 9);
return '<div class="music-widget" id="' + tvId + '">' +
'<div class="music-widget-header">' +
'<span class="music-widget-label">📈 ' + escHtml(tvSymbol) + ' · ' + escHtml(tvInterval) + '</span>' +
'<div style="display:flex;gap:6px;align-items:center">' +
'<button class="music-render-btn" onclick="renderTVChart(this,\'' + tvId + '-c\',\'' + escHtml(tvSymbol) + '\',\'' + escHtml(tvInterval) + '\')">📈 차트 불러오기</button>' +
'<button onclick="toggleMusicWidget(\'' + tvId + '\')" style="background:none;border:none;cursor:pointer;font-size:14px;color:var(--muted);padding:2px 4px" title="접기/펼치기">▾</button>' +
'<button onclick="closeMusicWidget(\'' + tvId + '\')" style="background:none;border:none;cursor:pointer;font-size:14px;color:var(--muted);padding:2px 4px" title="닫기">✕</button>' +
'</div>' +
'</div>' +
'<div class="music-widget-body">' +
'<div id="' + tvId + '-c" style="height:440px;width:100%"></div>' +
'</div>' +
'</div>';
}
return '<pre class="md-code-block"><code>' + trimmed + '</code></pre>';
});
@@ -9286,6 +9317,45 @@ function renderMarkdown(text, opts) {
return html;
}
function renderTVChart(btn, containerId, symbol, interval) {
btn.disabled = true;
btn.textContent = '로딩 중...';
var isDark = document.documentElement.getAttribute('data-theme') !== 'light';
function doInit() {
try {
new TradingView.widget({
container_id: containerId,
autosize: true,
symbol: symbol,
interval: interval || 'D',
timezone: 'Asia/Seoul',
theme: isDark ? 'dark' : 'light',
style: '1',
locale: 'kr',
enable_publishing: false,
allow_symbol_change: true,
save_image: false,
withdateranges: true,
hide_side_toolbar: false,
studies: ['RSI@tv-basicstudies', 'MACD@tv-basicstudies'],
});
btn.style.display = 'none';
} catch(e) {
btn.disabled = false;
btn.textContent = '📈 차트 불러오기';
}
}
if (window.TradingView && window.TradingView.widget) {
doInit();
} else {
var s = document.createElement('script');
s.src = 'https://s3.tradingview.com/tv.js';
s.onload = doInit;
s.onerror = function() { btn.disabled = false; btn.textContent = '📈 차트 불러오기'; };
document.head.appendChild(s);
}
}
function toggleMusicWidget(widgetId) {
var widget = document.getElementById(widgetId);
if (!widget) return;
@@ -9495,7 +9565,7 @@ function buildMusicActions(data, downloadName) {
+ '<span style="font-size:11px;color:var(--muted)" id="' + rid + '-st"></span>'
+ '</div>';
html += '<button class="music-dl-btn btn-green" id="' + rid + '-btn" onclick="midiRouterToggle(\'' + rid + '\',\'' + rid + '-btn\')">▶ 외부 신디</button>';
html += '<button class="music-dl-btn btn-purple" onclick="window.open(\'/midi-editor.html?url=\'+encodeURIComponent(\'' + midiUrl + '\'),\'_blank\')">🎹 편집</button>';
html += '<button class="music-dl-btn btn-purple" onclick="window.open(\'/midi-editor.html?url=\'+encodeURIComponent(\'' + midiUrl + '\'),\'_blank\',\'noopener,noreferrer\')">🎹 편집</button>';
html += '<a class="music-dl-btn" href="' + midiUrl + '" download="' + escHtml(downloadName || 'score.midi') + '">⬇ MIDI</a>';
}
if (pdfUrl) html += '<a class="music-dl-btn" href="' + pdfUrl + '" download="score.pdf">⬇ PDF</a>';
@@ -9616,7 +9686,7 @@ function buildMidiPickerWidget(data, filename) {
savedLine +
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">' +
'<button class="music-dl-btn btn-green" onclick="midiRouterToggle(\'' + rid + '\',\'' + rid + '\')">▶ 재생</button>' +
'<button class="music-dl-btn btn-purple" onclick="window.open(\'/midi-editor.html?url=\'+encodeURIComponent(\'' + midiUrl + '\'),\'_blank\')">🎹 편집</button>' +
'<button class="music-dl-btn btn-purple" onclick="window.open(\'/midi-editor.html?url=\'+encodeURIComponent(\'' + midiUrl + '\'),\'_blank\',\'noopener,noreferrer\')">🎹 편집</button>' +
'<button class="music-dl-btn btn-amber" id="' + wid + '-score-btn" onclick="midiPickerScore(\'' + wid + '\',\'' + data.id + '\',\'' + escHtml(filename) + '\')">' + scoreBtnLabel + '</button>' +
'</div>' +
playerPanel +
@@ -11061,6 +11131,10 @@ function canvasRun() {
});
})();
// Stop MIDI playback when tab is closed or navigated away
window.addEventListener('beforeunload', () => { try { midiRouter.stop(); } catch {} });
window.addEventListener('pagehide', () => { try { midiRouter.stop(); } catch {} });
// Resize xterm when window resizes
window.addEventListener('resize', () => {
const shell = getActiveShell();