149 lines
5.7 KiB
HTML
149 lines
5.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Python Runner</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap');
|
|
:root { --bg: #0c0c0c; --panel: #1a1a2e; --accent: #3b82f6; --green: #22c55e; --red: #ef4444; --muted: #6b7280; }
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
body { background:var(--bg); color:#e5e7eb; font-family:'IBM Plex Mono',monospace; height:100vh; display:flex; flex-direction:column; }
|
|
.header { display:flex; align-items:center; gap:12px; padding:10px 16px; background:var(--panel); border-bottom:1px solid #2d2d4a; flex-shrink:0; }
|
|
.header h1 { font-size:14px; font-weight:600; color:#e5e7eb; }
|
|
.header .file { font-size:12px; color:var(--accent); }
|
|
.status { font-size:11px; padding:2px 8px; border-radius:4px; font-weight:600; }
|
|
.status.loading { background:#1e3a5f; color:#60a5fa; }
|
|
.status.ready { background:#1a3a2a; color:var(--green); }
|
|
.status.running { background:#3a3a1a; color:#fbbf24; }
|
|
.status.error { background:#3a1a1a; color:var(--red); }
|
|
.toolbar { display:flex; gap:8px; margin-left:auto; }
|
|
.toolbar button { background:#252540; border:1px solid #3a3a5a; color:#e5e7eb; border-radius:6px; padding:6px 14px; font-size:12px; cursor:pointer; font-family:inherit; transition:all .15s; }
|
|
.toolbar button:hover { background:#353555; border-color:#4a4a7a; }
|
|
.toolbar button:disabled { opacity:.4; cursor:not-allowed; }
|
|
.toolbar button.run { background:#1a3a2a; border-color:#2d5a3a; color:var(--green); }
|
|
.toolbar button.run:hover { background:#224a3a; }
|
|
.output { flex:1; overflow:auto; padding:16px; font-size:13px; line-height:1.6; white-space:pre-wrap; word-break:break-all; }
|
|
.output .stdout { color:#e5e7eb; }
|
|
.output .stderr { color:var(--red); }
|
|
.output .info { color:var(--muted); font-style:italic; }
|
|
.footer { padding:8px 16px; border-top:1px solid #2d2d4a; font-size:11px; color:var(--muted); background:var(--panel); flex-shrink:0; display:flex; justify-content:space-between; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🐍 Python</h1>
|
|
<span class="file" id="filename"></span>
|
|
<span class="status loading" id="status">Pyodide 로딩중...</span>
|
|
<div class="toolbar">
|
|
<button class="run" id="btn-run" disabled onclick="runCode()">▶ 실행</button>
|
|
</div>
|
|
</div>
|
|
<div class="output" id="output"><span class="info">Pyodide를 로딩하고 있습니다...</span></div>
|
|
<div class="footer">
|
|
<span id="footer-info">Pyodide (WebAssembly Python)</span>
|
|
<span id="exec-time"></span>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.3/full/pyodide.js"></script>
|
|
<script>
|
|
let pyodide = null;
|
|
let code = sessionStorage.getItem('py_run_code') || '';
|
|
const fileName = sessionStorage.getItem('py_run_name') || 'script.py';
|
|
sessionStorage.removeItem('py_run_code');
|
|
sessionStorage.removeItem('py_run_name');
|
|
|
|
document.getElementById('filename').textContent = fileName;
|
|
document.title = fileName + ' — Python Runner';
|
|
|
|
function setStatus(text, cls) {
|
|
const el = document.getElementById('status');
|
|
el.textContent = text;
|
|
el.className = 'status ' + cls;
|
|
}
|
|
|
|
function appendOutput(text, cls = 'stdout') {
|
|
const el = document.getElementById('output');
|
|
const span = document.createElement('span');
|
|
span.className = cls;
|
|
span.textContent = text;
|
|
el.appendChild(span);
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
function clearOutput() {
|
|
document.getElementById('output').innerHTML = '';
|
|
}
|
|
|
|
async function initPyodide() {
|
|
try {
|
|
pyodide = await loadPyodide();
|
|
setStatus('준비', 'ready');
|
|
document.getElementById('btn-run').disabled = false;
|
|
const pyVer = pyodide.runPython('import sys; sys.version.split()[0]');
|
|
document.getElementById('footer-info').textContent = `Pyodide ${pyodide.version} (Python ${pyVer})`;
|
|
clearOutput();
|
|
if (code) {
|
|
appendOutput(`${fileName} 로드 완료 (${(new TextEncoder().encode(code).length / 1024).toFixed(1)}KB). ▶ 실행을 누르세요.\n`, 'info');
|
|
} else {
|
|
appendOutput('코드가 전달되지 않았습니다.\n', 'stderr');
|
|
setStatus('코드 없음', 'error');
|
|
}
|
|
} catch (e) {
|
|
setStatus('로딩 실패', 'error');
|
|
clearOutput();
|
|
appendOutput('Pyodide 로딩 실패:\n' + (e.stack || e.message || e) + '\n', 'stderr');
|
|
}
|
|
}
|
|
|
|
async function runCode() {
|
|
if (!pyodide) return;
|
|
if (!code) { appendOutput('실행할 코드가 없습니다.\n', 'stderr'); return; }
|
|
|
|
document.getElementById('btn-run').disabled = true;
|
|
clearOutput();
|
|
setStatus('실행중', 'running');
|
|
const t0 = performance.now();
|
|
|
|
try {
|
|
// Capture stdout/stderr
|
|
pyodide.runPython(`
|
|
import sys, io
|
|
_stdout_capture = io.StringIO()
|
|
_stderr_capture = io.StringIO()
|
|
sys.stdout = _stdout_capture
|
|
sys.stderr = _stderr_capture
|
|
`);
|
|
pyodide.runPython(code);
|
|
const stdout = pyodide.runPython('_stdout_capture.getvalue()');
|
|
const stderr = pyodide.runPython('_stderr_capture.getvalue()');
|
|
if (stdout) appendOutput(stdout);
|
|
if (stderr) appendOutput(stderr, 'stderr');
|
|
setStatus('완료', 'ready');
|
|
} catch (e) {
|
|
// Show full Python traceback
|
|
const tb = e.stack || e.message || String(e);
|
|
appendOutput(tb + '\n', 'stderr');
|
|
setStatus('오류', 'error');
|
|
} finally {
|
|
// Safely restore stdout
|
|
try {
|
|
pyodide.runPython(`
|
|
try:
|
|
sys.stdout = sys.__stdout__
|
|
sys.stderr = sys.__stderr__
|
|
except:
|
|
sys.stdout = io.StringIO()
|
|
sys.stderr = io.StringIO()
|
|
`);
|
|
} catch {}
|
|
const elapsed = ((performance.now() - t0) / 1000).toFixed(2);
|
|
document.getElementById('exec-time').textContent = `${elapsed}s`;
|
|
document.getElementById('btn-run').disabled = false;
|
|
}
|
|
}
|
|
|
|
initPyodide();
|
|
</script>
|
|
</body>
|
|
</html> |