<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>unhwp — HWP/HWPX WASM 플레이그라운드</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f1117; --surface: #1a1d27; --surface2: #242736;
--border: #2e3347; --accent: #4a9eff; --accent-dim: #2a5a99;
--text: #e2e4ee; --text-dim: #8890a8; --err: #ff6b6b; --ok: #6bffb3;
--radius: 10px; --mono: 'Fira Code', Consolas, monospace;
}
body { background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-height: 100vh; display: flex; flex-direction: column;
align-items: center; padding: 2rem 1rem; gap: 1.5rem; }
header { text-align: center; }
header h1 { font-size: 1.75rem; }
header h1 span { color: var(--accent); }
header p { color: var(--text-dim); margin-top: 0.35rem; font-size: 0.9rem; }
.drop-zone { width: 100%; max-width: 720px; border: 2px dashed var(--border);
border-radius: var(--radius); padding: 2.5rem 2rem; text-align: center;
cursor: pointer; transition: border-color 0.15s, background 0.15s;
background: var(--surface); user-select: none; }
.drop-zone:hover, .drop-zone.dragover { border-color: var(--accent); background: #1e2136; }
.drop-zone p { color: var(--text-dim); font-size: 0.9rem; }
.drop-zone p strong { color: var(--text); }
#file-input { display: none; }
.loading { width: 100%; max-width: 720px; text-align: center; color: var(--text-dim);
font-size: 0.9rem; }
.result-card { width: 100%; max-width: 720px; background: var(--surface);
border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.card-header { display: flex; align-items: center; justify-content: space-between;
padding: 0.75rem 1rem; background: var(--surface2); border-bottom: 1px solid var(--border);
gap: 0.5rem; flex-wrap: wrap; }
.file-info { font-size: 0.85rem; color: var(--text-dim); }
.file-info strong { color: var(--text); }
.tabs { display: flex; gap: 0.25rem; }
.tab { padding: 0.3rem 0.75rem; border-radius: 5px; border: 1px solid transparent;
background: transparent; color: var(--text-dim); font-size: 0.8rem; cursor: pointer;
transition: all 0.12s; }
.tab:hover { color: var(--text); background: var(--surface); }
.tab.active { color: var(--accent); border-color: var(--accent-dim); background: var(--surface); }
.content-area { padding: 1rem; max-height: 60vh; overflow-y: auto; }
pre { font-family: var(--mono); font-size: 0.82rem; white-space: pre-wrap;
word-break: break-word; line-height: 1.6; color: var(--text); }
.status-bar { padding: 0.5rem 1rem; font-size: 0.8rem; color: var(--text-dim);
border-top: 1px solid var(--border); background: var(--surface2); }
.ok { color: var(--ok); }
.error-card { width: 100%; max-width: 720px; background: #2a1a1a;
border: 1px solid var(--err); border-radius: var(--radius);
padding: 1rem; font-size: 0.9rem; color: var(--err); }
.hidden { display: none !important; }
</style>
</head>
<body>
<header>
<h1><span>unhwp</span> WASM 플레이그라운드</h1>
<p>HWP / HWPX 파일을 드래그하거나 클릭하여 Markdown으로 변환합니다.</p>
</header>
<div class="drop-zone" id="dropZone">
<p><strong>HWP 또는 HWPX 파일 선택</strong></p>
<p>드래그 앤 드롭 또는 클릭하여 업로드</p>
<input type="file" id="file-input" accept=".hwp,.hwpx">
</div>
<div class="loading hidden" id="loading">파싱 중...</div>
<div id="errorContainer"></div>
<div class="result-card hidden" id="resultCard">
<div class="card-header">
<div class="file-info" id="fileInfo"></div>
<div class="tabs">
<button class="tab active" data-tab="markdown">Markdown</button>
<button class="tab" data-tab="text">텍스트</button>
</div>
</div>
<div class="content-area">
<pre id="outputContent"></pre>
</div>
<div class="status-bar" id="statusBar"></div>
</div>
<script type="module">
import init, { parse } from './pkg/unhwp_wasm.js';
let wasmReady = false;
let tabs = { markdown: '', text: '' };
let activeTab = 'markdown';
async function initWasm() {
await init();
wasmReady = true;
}
initWasm().catch(err => showError(new Error(`WASM 로드 실패: ${err.message}`)));
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('file-input');
const loadingEl = document.getElementById('loading');
const resultCard = document.getElementById('resultCard');
const fileInfoEl = document.getElementById('fileInfo');
const outputContent = document.getElementById('outputContent');
const statusBarEl = document.getElementById('statusBar');
const errorContainer = document.getElementById('errorContainer');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files[0]) processFile(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', () => {
if (fileInput.files[0]) processFile(fileInput.files[0]);
});
document.querySelectorAll('.tab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
activeTab = btn.dataset.tab;
outputContent.textContent = tabs[activeTab] ?? '';
});
});
async function processFile(file) {
if (!wasmReady) { alert('WASM을 로드 중입니다. 잠시 후 다시 시도하세요.'); return; }
errorContainer.replaceChildren();
resultCard.classList.add('hidden');
loadingEl.classList.remove('hidden');
try {
const buf = await file.arrayBuffer();
const data = new Uint8Array(buf);
const doc = parse(data);
tabs.markdown = doc.toMarkdown();
tabs.text = doc.toText();
const sections = doc.sectionCount();
const paragraphs = doc.paragraphCount();
const nameEl = document.createElement('strong');
nameEl.textContent = file.name;
fileInfoEl.replaceChildren(nameEl, document.createTextNode(` (${formatBytes(file.size)})`));
outputContent.textContent = tabs[activeTab];
const checkEl = document.createElement('span');
checkEl.className = 'ok';
checkEl.textContent = '✓';
statusBarEl.replaceChildren(
checkEl,
document.createTextNode(` 섹션 ${sections}개 · 단락 ${paragraphs}개 · ${tabs.markdown.length.toLocaleString()} chars (MD)`)
);
loadingEl.classList.add('hidden');
resultCard.classList.remove('hidden');
} catch (err) {
loadingEl.classList.add('hidden');
showError(err);
}
}
function showError(err) {
const msg = err?.message ?? String(err);
let hint = '';
if (/unsupported|unknown format/i.test(msg)) hint = 'HWP 또는 HWPX 파일만 지원합니다.';
else if (/encrypt|password/i.test(msg)) hint = '암호화된 문서는 현재 지원하지 않습니다.';
const card = document.createElement('div');
card.className = 'error-card';
const heading = document.createElement('strong');
heading.textContent = '파싱 오류 ';
card.appendChild(heading);
card.appendChild(document.createTextNode(msg));
if (hint) {
const hintEl = document.createElement('span');
hintEl.style.cssText = 'color:var(--text-dim);display:block;margin-top:0.25rem';
hintEl.textContent = hint;
card.appendChild(hintEl);
}
errorContainer.replaceChildren(card);
}
function formatBytes(n) {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / 1024 / 1024).toFixed(1)} MB`;
}
</script>
</body>
</html>