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:
@@ -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 사이트에서 확인하세요"
|
||||||
|
- 외부 링크 안내
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 사용 가능한 도구
|
### 사용 가능한 도구
|
||||||
|
|
||||||
| 도구 | 활용 상황 |
|
| 도구 | 활용 상황 |
|
||||||
|
|||||||
@@ -5,6 +5,29 @@ emoji: "🎵"
|
|||||||
version: 1.0.0
|
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:gugak.go.kr` | 국립국악원 (국악 악보·음원·이론) |
|
||||||
| 저작권 정보 | `site:komca.or.kr` | 한국음악저작권협회 |
|
| 저작권 정보 | `site:komca.or.kr` | 한국음악저작권협회 |
|
||||||
| 재즈 리드시트 | `site:jazzstandards.com` | 재즈 스탠다드 코드·멜로디 분석 |
|
| 재즈 리드시트 | `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")`
|
예시: 베토벤 소나타 악보 → `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. **화성 진행** — 기능화성 또는 재즈/모달 화성 기반 코드 제안 (로마 숫자 표기 병기)
|
3. **화성 진행** — 기능화성 또는 재즈/모달 화성 기반 코드 제안 (로마 숫자 표기 병기)
|
||||||
4. **멜로디 아이디어** — 음역·리듬 패턴·모티프 발전 방향 제시
|
4. **멜로디 아이디어** — 음역·리듬 패턴·모티프 발전 방향 제시
|
||||||
5. **편성 조언** — 음역대·음색·밸런스 고려한 악기 배치
|
5. **편성 조언** — 음역대·음색·밸런스 고려한 악기 배치
|
||||||
6. **악보/리드시트** — 요청 시 ABC notation 또는 텍스트 악보로 표현
|
6. **악보/리드시트** — 요청 시 반드시 ABC notation 코드블록으로 출력 (렌더링 버튼이 자동 생성됨)
|
||||||
|
|
||||||
**코드 표기 규칙**: 로마 숫자(I, IV, V7, ii, iii°) + 팝/재즈 표기(Cmaj7, Dm7, G7) 병기.
|
**코드 표기 규칙**: 로마 숫자(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 분석 방법
|
### 악보 PDF 분석 방법
|
||||||
|
|||||||
+76
-2
@@ -9253,6 +9253,37 @@ function renderMarkdown(text, opts) {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'</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(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/gi,'"').replace(/'/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>';
|
return '<pre class="md-code-block"><code>' + trimmed + '</code></pre>';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -9286,6 +9317,45 @@ function renderMarkdown(text, opts) {
|
|||||||
return html;
|
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) {
|
function toggleMusicWidget(widgetId) {
|
||||||
var widget = document.getElementById(widgetId);
|
var widget = document.getElementById(widgetId);
|
||||||
if (!widget) return;
|
if (!widget) return;
|
||||||
@@ -9495,7 +9565,7 @@ function buildMusicActions(data, downloadName) {
|
|||||||
+ '<span style="font-size:11px;color:var(--muted)" id="' + rid + '-st"></span>'
|
+ '<span style="font-size:11px;color:var(--muted)" id="' + rid + '-st"></span>'
|
||||||
+ '</div>';
|
+ '</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-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>';
|
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>';
|
if (pdfUrl) html += '<a class="music-dl-btn" href="' + pdfUrl + '" download="score.pdf">⬇ PDF</a>';
|
||||||
@@ -9616,7 +9686,7 @@ function buildMidiPickerWidget(data, filename) {
|
|||||||
savedLine +
|
savedLine +
|
||||||
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">' +
|
'<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-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>' +
|
'<button class="music-dl-btn btn-amber" id="' + wid + '-score-btn" onclick="midiPickerScore(\'' + wid + '\',\'' + data.id + '\',\'' + escHtml(filename) + '\')">' + scoreBtnLabel + '</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
playerPanel +
|
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
|
// Resize xterm when window resizes
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
const shell = getActiveShell();
|
const shell = getActiveShell();
|
||||||
|
|||||||
Reference in New Issue
Block a user