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
|
||||
---
|
||||
|
||||
## ⚠️ 시스템 내장 악보 렌더링 — 반드시 읽을 것
|
||||
|
||||
이 시스템에는 **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
@@ -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(/&/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>';
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user